Skip to content

Features

You can enable, disable, or customize each feature through the features option.

Feature Overview

Vizel enables most features by default. Set any feature to false to disable it.

FeatureDescription
slashCommandSlash command menu (type / to open)
tableTable editing support
tableOfContentsAuto-collected heading navigation block
imageImage upload and resize
codeBlockCode blocks with syntax highlighting
dragHandleDrag handle for block reordering
characterCountCharacter and word counting
textColorText color and highlight
taskListCheckbox task lists
linkLink editing
markdownMarkdown import/export
mathematicsLaTeX math equations
embedURL embeds (YouTube, etc.)
detailsCollapsible content blocks
calloutInfo, warning, danger, tip, and note admonition blocks
diagramMermaid/GraphViz diagrams
superscriptSuperscript text formatting
subscriptSubscript text formatting
typographySmart quotes, em dashes, and other typographic transformations
wikiLinkWiki-style internal links ([[page-name]]) — opt-in
mention@user autocomplete — opt-in
commentText annotations and comments — opt-in
collaborationReal-time collaboration mode (disables History) — opt-in

Markdown Flavor Selection

Vizel supports multiple Markdown output flavors to match the platform you are targeting. The flavor option controls how content is serialized when exporting Markdown; input parsing is always tolerant and accepts all formats regardless of the selected flavor.

Available Flavors

FlavorCallout OutputWikiLink OutputTarget Platforms
"commonmark"Blockquote fallbackStandard link [text](wiki://page)Stack Overflow, Reddit, email
"gfm" (default)GitHub Alerts > [!NOTE]Standard link [text](wiki://page)GitHub, GitLab, DEV.to
"obsidian"Obsidian Callouts > [!note][[page]] / [[page|text]]Obsidian, Logseq, Foam
"docusaurus"Directives :::noteStandard link [text](wiki://page)Docusaurus, VitePress, Zenn, Qiita

Usage

typescript
const editor = useVizelEditor({
  flavor: 'obsidian', // Default: 'gfm'
});

Extension-Specific Behavior

  • Callout: Serializes admonition blocks in the format matching the selected flavor. All four callout formats are parsed regardless of flavor.
  • WikiLink: When flavor is "obsidian", wiki links serialize as [[page]] syntax. For all other flavors, they serialize as standard Markdown links [text](wiki://page).
  • Mention: Always serializes as @username regardless of flavor.

TIP

You can override the flavor-driven defaults for individual extensions. For example, set wikiLink.serializeAsWikiLink or callout.markdownFormat explicitly to override the flavor configuration.


Disabling Features

Set a feature to false to disable it:

typescript
const editor = useVizelEditor({
  features: {
    slashCommand: false,  // Disable slash commands
    dragHandle: false,    // Disable drag handle
    table: false,         // Disable tables
  },
});

Slash Commands

Type / to open the command menu for block insertion.

Options

PropertyTypeDescription
itemsVizelSlashCommandItem[]Custom command items
suggestionobjectSuggestion configuration

Custom Commands

typescript
import type { VizelSlashCommandItem } from '@vizel/core';

const customItems: VizelSlashCommandItem[] = [
  {
    title: 'Custom Block',
    description: 'Insert a custom block',
    icon: 'lucide:box',
    keywords: ['custom', 'block'],
    group: 'custom',
    command: ({ editor, range }) => {
      editor.chain().focus().deleteRange(range).insertContent({
        type: 'paragraph',
        content: [{ type: 'text', text: 'Custom content' }],
      }).run();
    },
  },
];

const editor = useVizelEditor({
  features: {
    slashCommand: {
      items: customItems, // Add custom items
    },
  },
});

Default Commands

GroupCommands
TextHeading 1, Heading 2, Heading 3
ListsBullet List, Numbered List, Task List
BlocksQuote, Divider, Details, Code Block, Table
MediaImage, Upload Image, Embed
AdvancedMath Equation, Inline Math, Mermaid Diagram, GraphViz Diagram

Images

The image feature supports drag and drop, paste, and resize.

Options

PropertyTypeDefaultDescription
resizebooleantrueEnable image resizing
onUpload(file: File) => Promise<string>Base64Upload handler
maxFileSizenumber-Max file size in bytes
allowedTypesstring[]See belowAllowed MIME types
onValidationError(error) => void-Validation error callback
onUploadError(error, file) => void-Upload error callback

Default Allowed Types

typescript
['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml']

Example: Custom Upload

typescript
const editor = useVizelEditor({
  features: {
    image: {
      onUpload: async (file) => {
        const formData = new FormData();
        formData.append('image', file);
        
        const res = await fetch('/api/upload', {
          method: 'POST',
          body: formData,
        });
        
        const { url } = await res.json();
        return url;
      },
      maxFileSize: 5 * 1024 * 1024, // 5MB
      allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
      onValidationError: (error) => {
        if (error.type === 'file_too_large') {
          alert('File is too large. Maximum size is 5MB.');
        } else if (error.type === 'invalid_type') {
          alert('Invalid file type. Only JPEG, PNG, and WebP are allowed.');
        }
      },
      onUploadError: (error, file) => {
        console.error(`Failed to upload ${file.name}:`, error);
        alert('Upload failed. Please try again.');
      },
    },
  },
});

Code Blocks

This feature provides code blocks with syntax highlighting and language selection.

Options

PropertyTypeDefaultDescription
defaultLanguagestring"plaintext"Default language
lineNumbersbooleanfalseShow line numbers
lowlightLowlightAll languagesCustom Lowlight instance

Example: Limited Languages

typescript
import { createLowlight, common } from 'lowlight';

const lowlight = createLowlight(common);

const editor = useVizelEditor({
  features: {
    codeBlock: {
      defaultLanguage: 'typescript',
      lineNumbers: true,
      lowlight, // Only common languages
    },
  },
});

Character Count

This feature tracks character and word counts.

Options

PropertyTypeDefaultDescription
limitnumber | nullnullMax characters (null = unlimited)
mode"textSize" | "nodeSize""textSize"Counting mode
wordCounter(text: string) => number-Custom word counter

Example: Character Limit

typescript
const editor = useVizelEditor({
  features: {
    characterCount: {
      limit: 10000, // Max 10,000 characters
      mode: 'textSize',
    },
  },
});

// Access counts
const chars = editor.storage.characterCount.characters();
const words = editor.storage.characterCount.words();
const limit = editor.storage.characterCount.limit;
const percentage = (chars / limit) * 100;

Text Color & Highlight

This feature adds text color and background highlight support.

Options

PropertyTypeDescription
textColorsVizelColorDefinition[]Custom text color palette
highlightColorsVizelColorDefinition[]Custom highlight color palette
multicolorbooleanEnable multicolor highlights

Custom Color Palette

typescript
import type { VizelColorDefinition } from '@vizel/core';

const customColors: VizelColorDefinition[] = [
  { name: 'Brand', color: '#6366f1' },
  { name: 'Success', color: '#22c55e' },
  { name: 'Warning', color: '#f59e0b' },
  { name: 'Error', color: '#ef4444' },
];

const editor = useVizelEditor({
  features: {
    textColor: {
      textColors: customColors,
      highlightColors: customColors.map(c => ({
        ...c,
        color: `${c.color}40`, // Add transparency
      })),
    },
  },
});

Markdown

This feature enables Markdown import/export.

Options

PropertyTypeDefaultDescription
indentation{ style, size }{ style: 'space', size: 2 }Indentation config
gfmbooleantrueGitHub Flavored Markdown
breaksbooleanfalseConvert newlines to <br>

Example

typescript
const editor = useVizelEditor({
  features: {
    markdown: {
      gfm: true,
      breaks: false,
      indentation: {
        style: 'space',
        size: 2,
      },
    },
  },
});

// Export to Markdown
const md = editor.storage.markdown.getMarkdown();

// Import from Markdown
editor.commands.setContent(
  editor.storage.markdown.parseMarkdown('# Hello\n\nWorld')
);

Recommended API

Use editor.getMarkdown() for Markdown export:

typescript
const md = editor.getMarkdown();

The editor.storage.markdown.getMarkdown() method is an internal storage accessor. Prefer editor.getMarkdown() for application code.


Mathematics

This feature renders LaTeX math equations with KaTeX.

Options

PropertyTypeDefaultDescription
katexOptionsKatexOptions{}KaTeX rendering options
inlineInputRulesbooleantrueEnable $...$ input rules
blockInputRulesbooleantrueEnable $$...$$ input rules

Example

typescript
const editor = useVizelEditor({
  features: {
    mathematics: {
      katexOptions: {
        throwOnError: false,
        strict: false,
      },
      inlineInputRules: true,
      blockInputRules: true,
    },
  },
});

Usage

  • Inline math: Type $E=mc^2$ and press space
  • Block math: Type $$ on a new line, enter equation, type $$

Embeds

This feature embeds content from URLs (YouTube, Vimeo, Twitter).

Options

PropertyTypeDefaultDescription
fetchEmbedDataFunctionBuilt-inCustom fetch function
providersVizelEmbedProvider[]Default providersCustom/additional providers
pasteHandlerbooleantrueAuto-embed pasted URLs
inlinebooleanfalseInline vs block embeds

Default Providers

  • YouTube
  • Vimeo
  • Twitter/X
  • CodePen
  • CodeSandbox
  • Figma
  • Loom
  • Spotify

Example: Custom Provider

typescript
import type { VizelEmbedProvider } from '@vizel/core';

const customProvider: VizelEmbedProvider = {
  name: 'MyService',
  patterns: [/^https?:\/\/myservice\.com\/embed\/(\w+)/],
  transform: (url) => `https://myservice.com/embed/${url.split('/').pop()}`,
};

const editor = useVizelEditor({
  features: {
    embed: {
      providers: [customProvider],
      pasteHandler: true,
    },
  },
});

Details

This feature provides collapsible content blocks (accordion/disclosure).

Options

PropertyTypeDescription
detailsobjectDetails container options
detailsContentobjectContent area options
detailsSummaryobjectSummary/header options

Example

typescript
const editor = useVizelEditor({
  features: {
    details: true, // Enable with defaults
  },
});

Usage

Use the slash command /details or /toggle to insert a collapsible block.


Diagrams

This feature supports Mermaid and GraphViz diagrams.

Options

PropertyTypeDefaultDescription
mermaidConfigMermaidConfig{}Mermaid configuration
graphvizEnginestring"dot"GraphViz layout engine
defaultType"mermaid" | "graphviz""mermaid"Default diagram type
defaultCodestring-Default Mermaid code
defaultGraphvizCodestring-Default GraphViz code

GraphViz Engines

  • dot - Hierarchical (default)
  • neato - Spring model
  • fdp - Force-directed
  • sfdp - Scalable force-directed
  • twopi - Radial
  • circo - Circular

Example

typescript
const editor = useVizelEditor({
  features: {
    diagram: {
      mermaidConfig: {
        theme: 'neutral',
        securityLevel: 'loose',
      },
      defaultType: 'mermaid',
      defaultCode: `graph TD
    A[Start] --> B[End]`,
    },
  },
});

This feature provides link editing and auto-linking.

Options

PropertyTypeDefaultDescription
openOnClickbooleantrueOpen links on click
autolinkbooleantrueAuto-link URLs while typing
linkOnPastebooleantrueLink pasted URLs
defaultProtocolstring"https"Default protocol
HTMLAttributesobject-HTML attributes for links

Example

typescript
const editor = useVizelEditor({
  features: {
    link: {
      openOnClick: true,
      autolink: true,
      linkOnPaste: true,
      defaultProtocol: 'https',
      HTMLAttributes: {
        target: '_blank',
        rel: 'noopener noreferrer',
      },
    },
  },
});

Task Lists

This feature adds checkbox task lists with nested indentation support.

Options

PropertyTypeDescription
taskListTaskListOptionsTask list container options
taskItemTaskItemOptionsTask item options

Keyboard Shortcuts

ShortcutAction
TabIndent (nest) the current task item
Shift+TabOutdent (un-nest) the current task item
Alt+↑Move the current task item up
Alt+↓Move the current task item down

These shortcuts also work for bullet lists and ordered lists.

Example

typescript
const editor = useVizelEditor({
  features: {
    taskList: {
      taskItem: {
        nested: true, // Allow nested task lists
      },
    },
  },
});

Drag Handle

This feature provides a handle for drag-and-drop block reordering. It also adds keyboard shortcuts for moving blocks and list items.

Options

PropertyTypeDefaultDescription
enabledbooleantrueShow drag handle

Keyboard Shortcuts

ShortcutAction
Alt+↑Move the current block or list item up
Alt+↓Move the current block or list item down
TabIndent (nest) a list item
Shift+TabOutdent (un-nest) a list item

The drag handle is automatically positioned next to the hovered block. For list items, it adjusts its size to be less intrusive.

Example

typescript
const editor = useVizelEditor({
  features: {
    dragHandle: {
      enabled: true,
    },
  },
});

Block Menu

Clicking the drag handle opens a context menu for the block. The menu provides:

  • Delete — Remove the block
  • Duplicate — Copy the block below
  • Copy / Cut — Clipboard operations
  • Turn into — Convert the block to a different type (heading, list, blockquote, etc.)

The block menu is automatically included in the Vizel all-in-one component. When using the composition pattern (VizelEditor + VizelBubbleMenu), add VizelBlockMenu explicitly:

tsx
// React
import { VizelBlockMenu } from '@vizel/react';
<VizelBlockMenu />

// Vue
import { VizelBlockMenu } from '@vizel/vue';
<VizelBlockMenu />

// Svelte
import { VizelBlockMenu } from '@vizel/svelte';
<VizelBlockMenu />

This feature adds wiki-style [[page-name]] links for linking between pages. It supports display text aliases with [[page-name|display text]] syntax.

Options

PropertyTypeDefaultDescription
resolveLink(pageName: string) => string(p) => '#' + pResolves a page name to a URL
pageExists(pageName: string) => boolean() => trueChecks if a page exists
getPageSuggestions(query: string) => VizelWikiLinkSuggestion[]-Autocomplete suggestions
onLinkClick(pageName: string, event: MouseEvent) => void-Click callback

Example

typescript
const editor = useVizelEditor({
  features: {
    wikiLink: {
      resolveLink: (page) => `/wiki/${encodeURIComponent(page)}`,
      pageExists: (page) => knownPages.has(page),
    },
  },
});

See the Wiki Links Guide for detailed usage.


Comments

This feature adds text annotation marks for reviewing and discussion.

Options

PropertyTypeDefaultDescription
enabledbooleantrueEnable comment marks

Example

typescript
const editor = useVizelEditor({
  features: {
    comment: true,
  },
});

Comment management (storage, replies, resolution) uses the framework-specific hooks/composables/runes. See the Comments Guide for details.


Collaboration

This feature enables real-time multi-user editing built on Yjs. When you enable it, Vizel disables the built-in History extension (Yjs handles undo/redo).

WARNING

This opt-in feature requires additional dependencies: yjs, a Yjs provider, @tiptap/extension-collaboration, and @tiptap/extension-collaboration-cursor.

Example

typescript
const editor = useVizelEditor({
  features: {
    collaboration: true, // Disables History
  },
  extensions: [
    Collaboration.configure({ document: ydoc }),
    CollaborationCursor.configure({ provider, user }),
  ],
});

See the Collaboration Guide for setup instructions.


Internationalization (i18n)

Vizel provides full internationalization support. All UI strings (toolbar labels, slash menu items, block menu actions, find & replace, etc.) can be translated by passing a VizelLocale object.

Quick Start

Pass a locale prop to the Vizel component or the editor hook/composable/rune:

tsx
import { Vizel } from '@vizel/react';
import type { VizelLocale } from '@vizel/core';
import { createVizelLocale } from '@vizel/core';

// Create a partial locale (only override what you need)
const jaLocale = createVizelLocale({
  toolbar: {
    undo: '元に戻す',
    redo: 'やり直す',
    bold: '太字',
    italic: '斜体',
  },
  slashMenu: {
    noResults: '結果なし',
    groups: { text: 'テキスト', lists: 'リスト', blocks: 'ブロック' },
  },
});

<Vizel locale={jaLocale} />
vue
<script setup lang="ts">
import { Vizel } from '@vizel/vue';
import { createVizelLocale } from '@vizel/core';

const jaLocale = createVizelLocale({
  toolbar: { undo: '元に戻す', redo: 'やり直す' },
});
</script>

<template>
  <Vizel :locale="jaLocale" />
</template>
svelte
<script lang="ts">
import { Vizel } from '@vizel/svelte';
import { createVizelLocale } from '@vizel/core';

const jaLocale = createVizelLocale({
  toolbar: { undo: '元に戻す', redo: 'やり直す' },
});
</script>

<Vizel locale={jaLocale} />

Full Locale Override

For a complete translation, provide a full VizelLocale object:

typescript
import type { VizelLocale } from '@vizel/core';

const myLocale: VizelLocale = {
  toolbar: { ariaLabel: 'Formatting', undo: 'Undo', redo: 'Redo', /* ... all fields required */ },
  nodeTypes: { text: 'Text', heading1: 'Heading 1', /* ... */ },
  blockMenu: { label: 'Block menu', delete: 'Delete', /* ... */ },
  slashMenu: { noResults: 'No results', groups: { /* ... */ }, items: { /* ... */ } },
  findReplace: { label: 'Find and replace', findPlaceholder: 'Find...', /* ... */ },
  codeBlock: { languagePlaceholder: 'Language', copyCode: 'Copy', copied: 'Copied!' },
  dragHandle: { ariaLabel: 'Drag to reorder block' },
  saveIndicator: { saved: 'Saved', saving: 'Saving...', unsaved: 'Unsaved', error: 'Error saving' },
  nodeSelector: { changeBlockType: 'Change block type', blockTypes: 'Block types', currentBlockType: 'Current block type: {type}' },
  relativeTime: { justNow: 'just now', secondsAgo: '{n}s ago', minutesAgo: '{n}m ago', hoursAgo: '{n}h ago', daysAgo: '{n}d ago' },
  bubbleMenu: { ariaLabel: 'Text formatting', superscript: 'Superscript', subscript: 'Subscript' },
  colorPicker: { textColor: 'Text Color', highlight: 'Highlight', textColorPalette: 'Text color palette', highlightPalette: 'Highlight color palette', recent: 'Recent', hexPlaceholder: '#000000', apply: 'Apply', applyAriaLabel: 'Apply custom color' },
  linkEditor: { urlPlaceholder: 'Enter URL...', apply: 'Apply', applyAriaLabel: 'Apply link', removeLink: 'Remove link', removeLinkAriaLabel: 'Remove link', openInNewTab: 'Open in new tab', visit: 'Visit', visitTitle: 'Open URL in new tab', embedAsRichContent: 'Embed as rich content' },
};

Partial Locale with createVizelLocale()

Use createVizelLocale() to merge partial translations with the English defaults:

typescript
import { createVizelLocale } from '@vizel/core';

// Only override the fields you need — English defaults fill the rest
const locale = createVizelLocale({
  toolbar: {
    bold: 'Fett',
    italic: 'Kursiv',
  },
  slashMenu: {
    noResults: 'Keine Ergebnisse',
  },
});

Composition Pattern

When using the decomposed component pattern, pass locale to the editor hook and to components that display UI strings:

tsx
import { VizelEditor, VizelBubbleMenu, VizelToolbar, VizelBlockMenu, useVizelEditor } from '@vizel/react';

const editor = useVizelEditor({ locale: myLocale });

<VizelToolbar editor={editor} locale={myLocale} />
<VizelEditor editor={editor} />
<VizelBubbleMenu editor={editor} locale={myLocale} />
<VizelBlockMenu locale={myLocale} />
vue
<VizelToolbar :editor="editor" :locale="myLocale" />
<VizelEditor :editor="editor" />
<VizelBubbleMenu :editor="editor" :locale="myLocale" />
<VizelBlockMenu :locale="myLocale" />
svelte
<VizelToolbar {editor} locale={myLocale} />
<VizelEditor {editor} />
<VizelBubbleMenu {editor} locale={myLocale} />
<VizelBlockMenu locale={myLocale} />

Default Locale

The English locale is available as vizelEnLocale:

typescript
import { vizelEnLocale } from '@vizel/core';

console.log(vizelEnLocale.toolbar.bold); // "Bold"

Toolbar Extensibility

The toolbar supports custom actions, dropdown menus, and responsive overflow for narrow viewports.

Custom Toolbar Actions

Pass an actions array to VizelToolbarDefault to customize the toolbar:

tsx
import { VizelToolbar, VizelToolbarDefault } from '@vizel/react';
import type { VizelToolbarActionItem } from '@vizel/core';
import { vizelDefaultToolbarActions } from '@vizel/core';

const myActions: VizelToolbarActionItem[] = [
  ...vizelDefaultToolbarActions,
  {
    id: 'myAction',
    label: 'My Action',
    icon: 'bold',
    group: 'custom',
    isActive: () => false,
    isEnabled: () => true,
    run: (editor) => { /* custom logic */ },
  },
];

<VizelToolbar>
  <VizelToolbarDefault editor={editor} actions={myActions} />
</VizelToolbar>
vue
<script setup lang="ts">
import { VizelToolbar, VizelToolbarDefault } from '@vizel/vue';
import type { VizelToolbarActionItem } from '@vizel/core';
import { vizelDefaultToolbarActions } from '@vizel/core';

const myActions: VizelToolbarActionItem[] = [
  ...vizelDefaultToolbarActions,
  {
    id: 'myAction',
    label: 'My Action',
    icon: 'bold',
    group: 'custom',
    isActive: () => false,
    isEnabled: () => true,
    run: (editor) => { /* custom logic */ },
  },
];
</script>
svelte
<script lang="ts">
import { VizelToolbar, VizelToolbarDefault } from '@vizel/svelte';
import type { VizelToolbarActionItem } from '@vizel/core';
import { vizelDefaultToolbarActions } from '@vizel/core';

const myActions: VizelToolbarActionItem[] = [
  ...vizelDefaultToolbarActions,
  {
    id: 'myAction',
    label: 'My Action',
    icon: 'bold',
    group: 'custom',
    isActive: () => false,
    isEnabled: () => true,
    run: (editor) => { /* custom logic */ },
  },
];
</script>

Group related actions into dropdown menus using VizelToolbarDropdownAction:

typescript
import type { VizelToolbarDropdownAction } from '@vizel/core';

const headingDropdown: VizelToolbarDropdownAction = {
  id: 'headings',
  label: 'Headings',
  icon: 'heading1',
  group: 'heading',
  type: 'dropdown',
  options: [
    {
      id: 'heading1',
      label: 'Heading 1',
      icon: 'heading1',
      group: 'heading',
      isActive: (editor) => editor.isActive('heading', { level: 1 }),
      isEnabled: (editor) => editor.can().toggleHeading({ level: 1 }),
      run: (editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
      shortcut: 'Mod+Alt+1',
    },
    // ... more heading options
  ],
  getActiveOption: (editor) =>
    // Return the currently active heading option to show in the trigger
    options.find((opt) => opt.isActive(editor)),
};

Overflow Menu

Use VizelToolbarOverflow to show hidden actions in a popover when the toolbar is narrow:

tsx
import { VizelToolbar, VizelToolbarOverflow } from '@vizel/react';

<VizelToolbar>
  {/* Main toolbar buttons */}
  <VizelToolbarOverflow editor={editor} actions={overflowActions} />
</VizelToolbar>
vue
<VizelToolbar>
  <!-- Main toolbar buttons -->
  <VizelToolbarOverflow :editor="editor" :actions="overflowActions" />
</VizelToolbar>
svelte
<VizelToolbar>
  <!-- Main toolbar buttons -->
  <VizelToolbarOverflow {editor} actions={overflowActions} />
</VizelToolbar>

Next Steps

Released under the MIT License.