v2.14.0

Editor

A rich text editor built on TipTap supporting markdown, HTML, and JSON content formats.

Usage

The Editor component provides a powerful rich text editing experience built on TipTap. It supports multiple content formats (JSON, HTML, Markdown), customizable toolbars, drag-and-drop block reordering, slash commands, mentions, emoji picker, and extensible architecture for adding custom functionality.

This example demonstrates a production-ready Editor component. Check out the source code on GitHub.
If you encounter prosemirror-related errors such as Adding different instances of a keyed plugin when using the Editor component or its extensions, you may need to add prosemirror packages to the vite.optimizeDeps.include list in your nuxt.config.ts file. This ensures Vite pre-bundles these dependencies to avoid loading multiple instances.
nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    optimizeDeps: {
      include: [
        '@bitrix24/b24ui-nuxt > prosemirror-state',
        '@bitrix24/b24ui-nuxt > prosemirror-transform',
        '@bitrix24/b24ui-nuxt > prosemirror-model',
        '@bitrix24/b24ui-nuxt > prosemirror-view',
        '@bitrix24/b24ui-nuxt > prosemirror-gapcursor'
      ]
    }
  }
})

Content

Use the v-model directive to control the value of the Editor.

<script setup lang="ts">
const value = ref({
  type: 'doc',
  content: [
    {
      type: 'heading',
      attrs: {
        level: 1
      },
      content: [
        {
          type: 'text',
          text: 'Hello World'
        }
      ]
    },
    {
      type: 'paragraph',
      content: [
        {
          type: 'text',
          text: 'This is a '
        },
        {
          type: 'text',
          marks: [
            {
              type: 'bold'
            }
          ],
          text: 'rich text'
        },
        {
          type: 'text',
          text: ' editor.'
        }
      ]
    }
  ]
})
</script>

<template>
  <B24Editor v-model="value" class="w-full min-h-21" />
</template>
<script setup lang="ts">
import { ref } from 'vue'

const value = ref({
  type: 'doc',
  content: [
    {
      type: 'heading',
      attrs: {
        level: 1
      },
      content: [
        {
          type: 'text',
          text: 'Hello World'
        }
      ]
    },
    {
      type: 'paragraph',
      content: [
        {
          type: 'text',
          text: 'This is a '
        },
        {
          type: 'text',
          marks: [
            {
              type: 'bold'
            }
          ],
          text: 'rich text'
        },
        {
          type: 'text',
          text: ' editor.'
        }
      ]
    }
  ]
})
</script>

<template>
  <B24Editor v-model="value" class="w-full min-h-21" />
</template>

Content Type

The Editor automatically detects the content format based on v-model type: strings are treated as html and objects as json.

You can explicitly set the format using the content-type prop: json, html, or markdown.

<script setup lang="ts">
const value = ref('<h1>Hello World</h1>\n<p>This is a <strong>rich text</strong> editor.</p>\n')
</script>

<template>
  <B24Editor v-model="value" content-type="html" class="w-full min-h-21" />
</template>
<script setup lang="ts">
import { ref } from 'vue'

const value = ref('<h1>Hello World</h1>\n<p>This is a <strong>rich text</strong> editor.</p>\n')
</script>

<template>
  <B24Editor v-model="value" content-type="html" class="w-full min-h-21" />
</template>

Extensions

The Editor includes the following extensions by default:

  • StarterKit - Core editing features (bold, italic, headings, lists, etc.)
  • Placeholder - Show placeholder text (when placeholder prop is provided)
  • Image - Insert and display images
  • Mention - Add @ mentions support
  • Markdown - Parse and serialize markdown (when content type is markdown)
Each built-in extension can be configured using its corresponding prop (starter-kit, placeholder, image, mention, markdown) to customize its behavior with TipTap options.

You can use the extensions prop to add additional TipTap extensions to enhance the Editor's capabilities:

<script setup lang="ts">
import { Emoji } from '@tiptap/extension-emoji'
import { TextAlign } from '@tiptap/extension-text-align'

const value = ref('<h1>Hello World</h1>\n')
</script>

<template>
  <B24Editor
    v-model="value"
    :extensions="[
      Emoji,
      TextAlign.configure({
        types: ['heading', 'paragraph']
      })
    ]"
  />
</template>
Check out the image upload example for creating custom TipTap extensions.

Placeholder

Use the placeholder prop to set a placeholder text that shows in empty paragraphs.

<script setup lang="ts">
const value = ref('')
</script>

<template>
  <B24Editor v-model="value" placeholder="Start writing..." class="w-full min-h-7" />
</template>
<script setup lang="ts">
import { ref } from 'vue'

const value = ref('')
</script>

<template>
  <B24Editor v-model="value" placeholder="Start writing..." class="w-full min-h-7" />
</template>
The placeholder prop accepts a string or an object with PlaceholderOptions and an additional mode property:
  • everyLine: Display placeholder on every empty line when focused (default).
  • firstLine: Display placeholder only on the first line when the editor is empty.
<template>
  <B24Editor :placeholder="{ placeholder: 'Start writing...', mode: 'firstLine' }" />
</template>
By default, placeholders only appear on top-level empty nodes. To show placeholders in nested elements like list items, set includeChildren to true:
<template>
  <B24Editor :placeholder="{ placeholder: 'Start writing...', includeChildren: true }" />
</template>
Learn more about Placeholder extension in the TipTap documentation.

Starter Kit

Use the starter-kit prop to configure the built-in TipTap StarterKit extension which includes common editor features like bold, italic, headings, lists, blockquotes, code blocks, and more.

<script setup lang="ts">
const value = ref('<h1>Hello World</h1>\n')
</script>

<template>
  <B24Editor
    v-model="value"
    :starter-kit="{
      blockquote: false,
      headings: {
        levels: [1, 2, 3, 4]
      },
      dropcursor: {
        color: 'var(--ui-color-accent-main-primary)',
        width: 2
      },
      link: {
        openOnClick: false
      }
    }"
  />
</template>
Set starter-kit to false for a plain text editor. It keeps the essential nodes (paragraph, text, history) and disables every formatting feature such as bold, italic, headings, lists, code, blockquote, links and horizontal rules.
Learn more about StarterKit extension in the TipTap documentation.

Handlers

Handlers wrap TipTap's built-in commands to provide a unified interface for editor actions. When you add a kind property to a EditorToolbar or EditorSuggestionMenu item, the corresponding handler executes the TipTap command and manages its state (active, disabled, etc.).

Default handlers

The Editor component provides these default handlers, which you can reference in toolbar or suggestion menu items using the kind property:

HandlerDescriptionUsage
markToggle text marks (bold, italic, strike, code, underline)Requires mark property in item
textAlignSet text alignment (left, center, right, justify)Requires align property in item
headingToggle heading levels (1-6)Requires level property in item
linkAdd, edit, or remove linksPrompts for URL if not provided
imageInsert imagesPrompts for URL if not provided
blockquoteToggle blockquotes
bulletListToggle bullet listsHandles list conversions
orderedListToggle ordered listsHandles list conversions
taskListToggle task listsHandles list conversions
codeBlockToggle code blocks
horizontalRuleInsert horizontal rules
paragraphSet paragraph format
undoUndo last change
redoRedo last undone change
clearFormattingRemove all formattingWorks with selection or position
duplicateDuplicate a nodeRequires pos property in item
deleteDelete a nodeRequires pos property in item
moveUpMove a node upRequires pos property in item
moveDownMove a node downRequires pos property in item
suggestionTrigger suggestion menuInserts / character
mentionTrigger mention menuInserts @ character
emojiTrigger emoji pickerInserts : character
The taskList and textAlign handlers only work when their respective extensions are installed, as they are not included in the Editor by default.

Here's how to use default handlers in toolbar or suggestion menu items:

<script setup lang="ts">
import type { EditorToolbarItem } from '@bitrix24/b24ui-nuxt'
import BoldIcon from '@bitrix24/b24icons-vue/outline/BoldIcon'
import ItalicIcon from '@bitrix24/b24icons-vue/outline/ItalicIcon'
import AlignLeftIcon from '@bitrix24/b24icons-vue/outline/AlignLeftIcon'
import AlignCenterIcon from '@bitrix24/b24icons-vue/outline/AlignCenterIcon'
import BulletedListIcon from '@bitrix24/b24icons-vue/outline/BulletedListIcon'
import NumberedListIcon from '@bitrix24/b24icons-vue/outline/NumberedListIcon'
import QuoteIcon from '@bitrix24/b24icons-vue/outline/QuoteIcon'
import LinkIcon from '@bitrix24/b24icons-vue/outline/LinkIcon'

const value = ref('<h1>Hello World</h1>\n')

const items: EditorToolbarItem[] = [
  { kind: 'mark', mark: 'bold', icon: BoldIcon },
  { kind: 'mark', mark: 'italic', icon: ItalicIcon },
  { kind: 'heading', level: 1 },
  { kind: 'heading', level: 2 },
  { kind: 'textAlign', align: 'left', icon: AlignLeftIcon },
  { kind: 'textAlign', align: 'center', icon: AlignCenterIcon },
  { kind: 'bulletList', icon: BulletedListIcon },
  { kind: 'orderedList', icon: NumberedListIcon },
  { kind: 'blockquote', icon: QuoteIcon },
  { kind: 'link', icon: LinkIcon }
]
</script>

<template>
  <B24Editor v-slot="{ editor }" v-model="value">
    <B24EditorToolbar :editor="editor" :items="items" />
  </B24Editor>
</template>

Custom handlers

Use the handlers prop to extend or override the default handlers. Custom handlers are merged with the default handlers, so you can add new actions or modify existing behavior.

Each handler implements the EditorHandler interface:

interface EditorHandler {
  /* Checks if the command can be executed in the current editor state */
  canExecute: (editor: Editor, item?: any) => boolean
  /* Executes the command and returns a Tiptap chain */
  execute: (editor: Editor, item?: any) => any
  /* Determines if the item should appear active (used for toggle states) */
  isActive: (editor: Editor, item?: any) => boolean
  /* Optional additional check to disable the item (combined with `canExecute`) */
  isDisabled?: (editor: Editor, item?: any) => boolean
}

Here's an example of creating custom handlers:

<script setup lang="ts">
import type { Editor } from '@tiptap/vue-3'
import type { EditorCustomHandlers, EditorToolbarItem } from '@bitrix24/b24ui-nuxt'
import BoldIcon from '@bitrix24/b24icons-vue/outline/BoldIcon'
import TextColorIcon from '@bitrix24/b24icons-vue/editor/TextColorIcon'

const value = ref('<h1>Hello World</h1>\n')

const customHandlers = {
  highlight: {
    canExecute: (editor: Editor) => editor.can().toggleHighlight(),
    execute: (editor: Editor) => editor.chain().focus().toggleHighlight(),
    isActive: (editor: Editor) => editor.isActive('highlight'),
    isDisabled: (editor: Editor) => !editor.isEditable
  }
} satisfies EditorCustomHandlers

const items = [
  // Built-in handler
  { kind: 'mark', mark: 'bold', icon: BoldIcon },
  // Custom handler
  { kind: 'highlight', icon: TextColorIcon }
] satisfies EditorToolbarItem<typeof customHandlers>[]
</script>

<template>
  <B24Editor v-slot="{ editor }" v-model="value" :handlers="customHandlers">
    <B24EditorToolbar :editor="editor" :items="items" />
  </B24Editor>
</template>
Check out the image upload example for a complete implementation with custom handlers.

Examples

With toolbar

You can use the EditorToolbar component to add a fixed, bubble, or floating toolbar to the Editor with common formatting actions.

<script setup lang="ts">
import type { EditorToolbarItem } from '@bitrix24/b24ui-nuxt'
import HeaderIcon from '@bitrix24/b24icons-vue/editor/HeaderIcon'
import BoldIcon from '@bitrix24/b24icons-vue/outline/BoldIcon'
import ItalicIcon from '@bitrix24/b24icons-vue/outline/ItalicIcon'
import UnderlineIcon from '@bitrix24/b24icons-vue/outline/UnderlineIcon'
import StrikethroughIcon from '@bitrix24/b24icons-vue/outline/StrikethroughIcon'
import DeveloperResourcesIcon from '@bitrix24/b24icons-vue/outline/DeveloperResourcesIcon'

const value = ref(`# Toolbar

Select some text to see the formatting toolbar appear above your selection.`)

const items: EditorToolbarItem[][] = [
  [
    {
      icon: HeaderIcon,
      tooltip: { text: 'Headings' },
      content: { align: 'start' },
      items: [
        {
          kind: 'heading',
          level: 1,
          label: 'Heading 1'
        },
        {
          kind: 'heading',
          level: 2,
          label: 'Heading 2'
        },
        {
          kind: 'heading',
          level: 3,
          label: 'Heading 3'
        },
        {
          kind: 'heading',
          level: 4,
          label: 'Heading 4'
        }
      ]
    }
  ],
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: BoldIcon,
      tooltip: { text: 'Bold' }
    },
    {
      kind: 'mark',
      mark: 'italic',
      icon: ItalicIcon,
      tooltip: { text: 'Italic' }
    },
    {
      kind: 'mark',
      mark: 'underline',
      icon: UnderlineIcon,
      tooltip: { text: 'Underline' }
    },
    {
      kind: 'mark',
      mark: 'strike',
      icon: StrikethroughIcon,
      tooltip: { text: 'Strikethrough' }
    },
    {
      kind: 'mark',
      mark: 'code',
      icon: DeveloperResourcesIcon,
      tooltip: { text: 'Code' }
    }
  ]
]
</script>

<template>
  <B24Editor v-slot="{ editor }" v-model="value" content-type="markdown" class="w-full min-h-21">
    <B24EditorToolbar :editor="editor" :items="items" layout="bubble" />
  </B24Editor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorToolbarItem } from '@bitrix24/b24ui-nuxt'
import HeaderIcon from '@bitrix24/b24icons-vue/editor/HeaderIcon'
import BoldIcon from '@bitrix24/b24icons-vue/outline/BoldIcon'
import ItalicIcon from '@bitrix24/b24icons-vue/outline/ItalicIcon'
import UnderlineIcon from '@bitrix24/b24icons-vue/outline/UnderlineIcon'
import StrikethroughIcon from '@bitrix24/b24icons-vue/outline/StrikethroughIcon'
import DeveloperResourcesIcon from '@bitrix24/b24icons-vue/outline/DeveloperResourcesIcon'

const value = ref(`# Toolbar

Select some text to see the formatting toolbar appear above your selection.`)

const items: EditorToolbarItem[][] = [
  [
    {
      icon: HeaderIcon,
      tooltip: { text: 'Headings' },
      content: { align: 'start' },
      items: [
        {
          kind: 'heading',
          level: 1,
          label: 'Heading 1'
        },
        {
          kind: 'heading',
          level: 2,
          label: 'Heading 2'
        },
        {
          kind: 'heading',
          level: 3,
          label: 'Heading 3'
        },
        {
          kind: 'heading',
          level: 4,
          label: 'Heading 4'
        }
      ]
    }
  ],
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: BoldIcon,
      tooltip: { text: 'Bold' }
    },
    {
      kind: 'mark',
      mark: 'italic',
      icon: ItalicIcon,
      tooltip: { text: 'Italic' }
    },
    {
      kind: 'mark',
      mark: 'underline',
      icon: UnderlineIcon,
      tooltip: { text: 'Underline' }
    },
    {
      kind: 'mark',
      mark: 'strike',
      icon: StrikethroughIcon,
      tooltip: { text: 'Strikethrough' }
    },
    {
      kind: 'mark',
      mark: 'code',
      icon: DeveloperResourcesIcon,
      tooltip: { text: 'Code' }
    }
  ]
]
</script>

<template>
  <B24Editor v-slot="{ editor }" v-model="value" content-type="markdown" class="w-full min-h-21">
    <B24EditorToolbar :editor="editor" :items="items" layout="bubble" />
  </B24Editor>
</template>

With drag handle

You can use the EditorDragHandle component to add a draggable handle for reordering blocks.

<script setup lang="ts">
const value = ref(`# Drag Handle

Hover over the left side of this block to see the drag handle appear and reorder blocks.`)
</script>

<template>
  <B24Editor v-slot="{ editor }" v-model="value" content-type="markdown" class="w-full min-h-21">
    <B24EditorDragHandle :editor="editor" />
  </B24Editor>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const value = ref(`# Drag Handle

Hover over the left side of this block to see the drag handle appear and reorder blocks.`)
</script>

<template>
  <B24Editor v-slot="{ editor }" v-model="value" content-type="markdown" class="w-full min-h-21">
    <B24EditorDragHandle :editor="editor" />
  </B24Editor>
</template>

With suggestion menu

You can use the EditorSuggestionMenu component to add slash commands for quick formatting and insertions.

<script setup lang="ts">
import type { EditorSuggestionMenuItem } from '@bitrix24/b24ui-nuxt'
import TextIcon from '@bitrix24/b24icons-vue/outline/TextIcon'
import BulletedListIcon from '@bitrix24/b24icons-vue/outline/BulletedListIcon'
import NumberedListIcon from '@bitrix24/b24icons-vue/outline/NumberedListIcon'
import QuoteIcon from '@bitrix24/b24icons-vue/outline/QuoteIcon'
import CodeIcon from '@bitrix24/b24icons-vue/common-service/CodeIcon'
import HrIcon from '@bitrix24/b24icons-vue/editor/HrIcon'

const value = ref(`# Suggestion Menu

Type / to open the suggestion menu and browse available formatting commands.`)

const items: EditorSuggestionMenuItem[][] = [
  [
    {
      type: 'label',
      label: 'Text'
    },
    {
      kind: 'paragraph',
      label: 'Paragraph',
      icon: TextIcon
    },
    {
      kind: 'heading',
      level: 1,
      label: 'Heading 1'
    },
    {
      kind: 'heading',
      level: 2,
      label: 'Heading 2'
    },
    {
      kind: 'heading',
      level: 3,
      label: 'Heading 3'
    }
  ],
  [
    {
      type: 'label',
      label: 'Lists'
    },
    {
      kind: 'bulletList',
      label: 'Bullet List',
      icon: BulletedListIcon
    },
    {
      kind: 'orderedList',
      label: 'Numbered List',
      icon: NumberedListIcon
    }
  ],
  [
    {
      type: 'label',
      label: 'Insert'
    },
    {
      kind: 'blockquote',
      label: 'Blockquote',
      icon: QuoteIcon
    },
    {
      kind: 'codeBlock',
      label: 'Code Block',
      icon: CodeIcon
    },
    {
      kind: 'horizontalRule',
      label: 'Divider',
      icon: HrIcon
    }
  ]
]

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <B24Editor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type / for commands..."
    class="w-full min-h-21"
  >
    <B24EditorSuggestionMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </B24Editor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorSuggestionMenuItem } from '@bitrix24/b24ui-nuxt'
import TextIcon from '@bitrix24/b24icons-vue/outline/TextIcon'
import BulletedListIcon from '@bitrix24/b24icons-vue/outline/BulletedListIcon'
import NumberedListIcon from '@bitrix24/b24icons-vue/outline/NumberedListIcon'
import QuoteIcon from '@bitrix24/b24icons-vue/outline/QuoteIcon'
import CodeIcon from '@bitrix24/b24icons-vue/common-service/CodeIcon'
import HrIcon from '@bitrix24/b24icons-vue/editor/HrIcon'

const value = ref(`# Suggestion Menu

Type / to open the suggestion menu and browse available formatting commands.`)

const items: EditorSuggestionMenuItem[][] = [
  [
    {
      type: 'label',
      label: 'Text'
    },
    {
      kind: 'paragraph',
      label: 'Paragraph',
      icon: TextIcon
    },
    {
      kind: 'heading',
      level: 1,
      label: 'Heading 1'
    },
    {
      kind: 'heading',
      level: 2,
      label: 'Heading 2'
    },
    {
      kind: 'heading',
      level: 3,
      label: 'Heading 3'
    }
  ],
  [
    {
      type: 'label',
      label: 'Lists'
    },
    {
      kind: 'bulletList',
      label: 'Bullet List',
      icon: BulletedListIcon
    },
    {
      kind: 'orderedList',
      label: 'Numbered List',
      icon: NumberedListIcon
    }
  ],
  [
    {
      type: 'label',
      label: 'Insert'
    },
    {
      kind: 'blockquote',
      label: 'Blockquote',
      icon: QuoteIcon
    },
    {
      kind: 'codeBlock',
      label: 'Code Block',
      icon: CodeIcon
    },
    {
      kind: 'horizontalRule',
      label: 'Divider',
      icon: HrIcon
    }
  ]
]

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <B24Editor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type / for commands..."
    class="w-full min-h-21"
  >
    <B24EditorSuggestionMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </B24Editor>
</template>

With mention menu

You can use the EditorMentionMenu component to add @ mentions for tagging users or entities.

<script setup lang="ts">
import type { EditorMentionMenuItem } from '@bitrix24/b24ui-nuxt'

const value = ref(`# Mention Menu

Type @ to mention someone and select from the list of available users.`)

const items: EditorMentionMenuItem[] = [
  {
    label: 'bitrix24',
    avatar: {
      src: 'https://github.com/bitrix24.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Employee',
    avatar: {
      src: '/b24ui/avatar/employee.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Assistant',
    avatar: {
      src: '/b24ui/avatar/assistant.png',
      loading: 'lazy' as const
    }
  }
]

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <B24Editor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type @ to mention someone..."
    class="w-full min-h-21"
  >
    <B24EditorMentionMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </B24Editor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorMentionMenuItem } from '@bitrix24/b24ui-nuxt'

const value = ref(`# Mention Menu

Type @ to mention someone and select from the list of available users.`)

const items: EditorMentionMenuItem[] = [
  {
    label: 'bitrix24',
    avatar: {
      src: 'https://github.com/bitrix24.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Employee',
    avatar: {
      src: '/b24ui/avatar/employee.png',
      loading: 'lazy' as const
    }
  },
  {
    label: 'Assistant',
    avatar: {
      src: '/b24ui/avatar/assistant.png',
      loading: 'lazy' as const
    }
  }
]

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <B24Editor
    v-slot="{ editor }"
    v-model="value"
    content-type="markdown"
    placeholder="Type @ to mention someone..."
    class="w-full min-h-21"
  >
    <B24EditorMentionMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </B24Editor>
</template>

With emoji menu

You can use the EditorEmojiMenu component to add emoji picker support.

<script setup lang="ts">
import type { EditorEmojiMenuItem } from '@bitrix24/b24ui-nuxt'
import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji'

const value = ref(`# Emoji Menu

Type : to insert emojis and select from the list of available emojis.`)

const items: EditorEmojiMenuItem[] = gitHubEmojis.filter(
  (emoji) => !emoji.name.startsWith('regional_indicator_')
)

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <B24Editor
    v-slot="{ editor }"
    v-model="value"
    :extensions="[Emoji]"
    content-type="markdown"
    placeholder="Type : to add emojis..."
    class="w-full min-h-21"
  >
    <B24EditorEmojiMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </B24Editor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorEmojiMenuItem } from '@bitrix24/b24ui-nuxt'
import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji'

const value = ref(`# Emoji Menu

Type : to insert emojis and select from the list of available emojis.`)

const items: EditorEmojiMenuItem[] = gitHubEmojis.filter(
  (emoji) => !emoji.name.startsWith('regional_indicator_')
)

// SSR-safe function to append menus to body (avoids z-index issues in docs)
const appendToBody = import.meta.client ? () => document.body : undefined
</script>

<template>
  <B24Editor
    v-slot="{ editor }"
    v-model="value"
    :extensions="[Emoji]"
    content-type="markdown"
    placeholder="Type : to add emojis..."
    class="w-full min-h-21"
  >
    <B24EditorEmojiMenu :editor="editor" :items="items" :append-to="appendToBody" />
  </B24Editor>
</template>

With image upload

This example demonstrates how to create an image upload feature using the extensions prop to register a custom TipTap node and the handlers prop to define how the toolbar button triggers the upload flow.

  1. Create a Vue component that uses the FileUpload component:
EditorImageUploadNode.vue
<script setup lang="ts">
import type { NodeViewProps } from '@tiptap/vue-3'
import { NodeViewWrapper } from '@tiptap/vue-3'
import SpinnerIcon from '@bitrix24/b24icons-vue/specialized/SpinnerIcon'
import ImageIcon from '@bitrix24/b24icons-vue/outline/ImageIcon'

const props = defineProps<NodeViewProps>()

const file = ref<File | null>(null)
const loading = ref(false)

watch(file, async (newFile) => {
  if (!newFile) return

  loading.value = true

  const reader = new FileReader()
  reader.onload = async (e) => {
    const dataUrl = e.target?.result as string
    if (!dataUrl) {
      loading.value = false
      return
    }

    // Simulate upload delay
    await new Promise(resolve => setTimeout(resolve, 1000))

    const pos = props.getPos()
    if (typeof pos !== 'number') {
      loading.value = false
      return
    }

    props.editor
      .chain()
      .focus()
      .deleteRange({ from: pos, to: pos + 1 })
      .setImage({ src: dataUrl })
      .run()

    loading.value = false
  }
  reader.readAsDataURL(newFile)
})
</script>

<template>
  <NodeViewWrapper>
    <B24FileUpload
      v-model="file"
      accept="image/*"
      label="Upload an image"
      description="SVG, PNG, JPG or GIF (max. 2MB)"
      :preview="false"
      class="min-h-48"
    >
      <template #leading>
        <B24Avatar
          :icon="loading ? SpinnerIcon : ImageIcon"
          size="md"
          :b24ui="{ icon: [loading && 'animate-spin'] }"
        />
      </template>
    </B24FileUpload>
  </NodeViewWrapper>
</template>
EditorImageUploadNode.vue
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { NodeViewProps } from '@tiptap/vue-3'
import { NodeViewWrapper } from '@tiptap/vue-3'
import SpinnerIcon from '@bitrix24/b24icons-vue/specialized/SpinnerIcon'
import ImageIcon from '@bitrix24/b24icons-vue/outline/ImageIcon'

const props = defineProps<NodeViewProps>()

const file = ref<File | null>(null)
const loading = ref(false)

watch(file, async (newFile) => {
  if (!newFile) return

  loading.value = true

  const reader = new FileReader()
  reader.onload = async (e) => {
    const dataUrl = e.target?.result as string
    if (!dataUrl) {
      loading.value = false
      return
    }

    // Simulate upload delay
    await new Promise(resolve => setTimeout(resolve, 1000))

    const pos = props.getPos()
    if (typeof pos !== 'number') {
      loading.value = false
      return
    }

    props.editor
      .chain()
      .focus()
      .deleteRange({ from: pos, to: pos + 1 })
      .setImage({ src: dataUrl })
      .run()

    loading.value = false
  }
  reader.readAsDataURL(newFile)
})
</script>

<template>
  <NodeViewWrapper>
    <B24FileUpload
      v-model="file"
      accept="image/*"
      label="Upload an image"
      description="SVG, PNG, JPG or GIF (max. 2MB)"
      :preview="false"
      class="min-h-48"
    >
      <template #leading>
        <B24Avatar
          :icon="loading ? SpinnerIcon : ImageIcon"
          size="md"
          :b24ui="{ icon: [loading && 'animate-spin'] }"
        />
      </template>
    </B24FileUpload>
  </NodeViewWrapper>
</template>
  1. Create a custom TipTap extension to register the node:
EditorImageUploadExtension.ts
import { Node, mergeAttributes } from '@tiptap/core'
import type { CommandProps, NodeViewRenderer } from '@tiptap/core'
import { VueNodeViewRenderer } from '@tiptap/vue-3'
import ImageUploadNodeComponent from './EditorImageUploadNode.vue'

declare module '@tiptap/core' {
  interface Commands<ReturnType> {
    imageUpload: {
      insertImageUpload: () => ReturnType
    }
  }
}

export const ImageUpload = Node.create({
  name: 'imageUpload',
  group: 'block',
  atom: true,
  draggable: true,
  addAttributes() {
    return {}
  },
  parseHTML() {
    return [{
      tag: 'div[data-type="image-upload"]'
    }]
  },
  renderHTML({ HTMLAttributes }) {
    return ['div', mergeAttributes(HTMLAttributes, { 'data-type': 'image-upload' })]
  },
  addNodeView(): NodeViewRenderer {
    return VueNodeViewRenderer(ImageUploadNodeComponent)
  },
  addCommands() {
    return {
      insertImageUpload: () => ({ commands }: CommandProps) => {
        return commands.insertContent({ type: this.name })
      }
    }
  }
})

export default ImageUpload
  1. Use the custom extension in the Editor:
<script setup lang="ts">
import type { EditorCustomHandlers, EditorToolbarItem } from '@bitrix24/b24ui-nuxt'
import type { Editor } from '@tiptap/vue-3'
import { ImageUpload } from './EditorImageUploadExtension'
import HeaderIcon from '@bitrix24/b24icons-vue/editor/HeaderIcon'
import BoldIcon from '@bitrix24/b24icons-vue/outline/BoldIcon'
import ItalicIcon from '@bitrix24/b24icons-vue/outline/ItalicIcon'
import UnderlineIcon from '@bitrix24/b24icons-vue/outline/UnderlineIcon'
import StrikethroughIcon from '@bitrix24/b24icons-vue/outline/StrikethroughIcon'
import DeveloperResourcesIcon from '@bitrix24/b24icons-vue/outline/DeveloperResourcesIcon'
import ImageIcon from '@bitrix24/b24icons-vue/outline/ImageIcon'

const value = ref(`# Image Upload

This editor demonstrates how to create a custom TipTap extension with handlers. Click the image button in the toolbar to upload a file — it will show a custom [FileUpload](/docs/components/file-upload) interface before inserting the image.

Try uploading an image below:

`)

const customHandlers = {
  imageUpload: {
    canExecute: (editor: Editor) => editor.can().insertContent({ type: 'imageUpload' }),
    execute: (editor: Editor) => editor.chain().focus().insertContent({ type: 'imageUpload' }),
    isActive: (editor: Editor) => editor.isActive('imageUpload'),
    isDisabled: undefined
  }
} satisfies EditorCustomHandlers

const items = [
  [
    {
      kind: 'imageUpload',
      icon: ImageIcon,
      label: 'Add image'
    }
  ],
  [
    {
      icon: HeaderIcon,
      content: { align: 'start' },
      items: [
        {
          kind: 'heading',
          level: 1,
          label: 'Heading 1'
        },
        {
          kind: 'heading',
          level: 2,
          label: 'Heading 2'
        },
        {
          kind: 'heading',
          level: 3,
          label: 'Heading 3'
        },
        {
          kind: 'heading',
          level: 4,
          label: 'Heading 4'
        }
      ]
    }
  ],
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: BoldIcon
    },
    {
      kind: 'mark',
      mark: 'italic',
      icon: ItalicIcon
    },
    {
      kind: 'mark',
      mark: 'underline',
      icon: UnderlineIcon
    },
    {
      kind: 'mark',
      mark: 'strike',
      icon: StrikethroughIcon
    },
    {
      kind: 'mark',
      mark: 'code',
      icon: DeveloperResourcesIcon
    }
  ]
] satisfies EditorToolbarItem<typeof customHandlers>[][]
</script>

<template>
  <B24Editor
    v-slot="{ editor }"
    v-model="value"
    :extensions="[ImageUpload]"
    :handlers="customHandlers"
    content-type="markdown"
    :b24ui="{ base: 'p-8 sm:px-16' }"
    class="w-full min-h-74"
  >
    <B24EditorToolbar
      :editor="editor"
      :items="items"
      class="border-b border-muted bg-default py-2 px-8 sm:px-16 overflow-x-auto"
    />
  </B24Editor>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { EditorCustomHandlers, EditorToolbarItem } from '@bitrix24/b24ui-nuxt'
import type { Editor } from '@tiptap/vue-3'
import { ImageUpload } from './EditorImageUploadExtension'
import HeaderIcon from '@bitrix24/b24icons-vue/editor/HeaderIcon'
import BoldIcon from '@bitrix24/b24icons-vue/outline/BoldIcon'
import ItalicIcon from '@bitrix24/b24icons-vue/outline/ItalicIcon'
import UnderlineIcon from '@bitrix24/b24icons-vue/outline/UnderlineIcon'
import StrikethroughIcon from '@bitrix24/b24icons-vue/outline/StrikethroughIcon'
import DeveloperResourcesIcon from '@bitrix24/b24icons-vue/outline/DeveloperResourcesIcon'
import ImageIcon from '@bitrix24/b24icons-vue/outline/ImageIcon'

const value = ref(`# Image Upload

This editor demonstrates how to create a custom TipTap extension with handlers. Click the image button in the toolbar to upload a file — it will show a custom [FileUpload](/docs/components/file-upload) interface before inserting the image.

Try uploading an image below:

`)

const customHandlers = {
  imageUpload: {
    canExecute: (editor: Editor) => editor.can().insertContent({ type: 'imageUpload' }),
    execute: (editor: Editor) => editor.chain().focus().insertContent({ type: 'imageUpload' }),
    isActive: (editor: Editor) => editor.isActive('imageUpload'),
    isDisabled: undefined
  }
} satisfies EditorCustomHandlers

const items = [
  [
    {
      kind: 'imageUpload',
      icon: ImageIcon,
      label: 'Add image'
    }
  ],
  [
    {
      icon: HeaderIcon,
      content: { align: 'start' },
      items: [
        {
          kind: 'heading',
          level: 1,
          label: 'Heading 1'
        },
        {
          kind: 'heading',
          level: 2,
          label: 'Heading 2'
        },
        {
          kind: 'heading',
          level: 3,
          label: 'Heading 3'
        },
        {
          kind: 'heading',
          level: 4,
          label: 'Heading 4'
        }
      ]
    }
  ],
  [
    {
      kind: 'mark',
      mark: 'bold',
      icon: BoldIcon
    },
    {
      kind: 'mark',
      mark: 'italic',
      icon: ItalicIcon
    },
    {
      kind: 'mark',
      mark: 'underline',
      icon: UnderlineIcon
    },
    {
      kind: 'mark',
      mark: 'strike',
      icon: StrikethroughIcon
    },
    {
      kind: 'mark',
      mark: 'code',
      icon: DeveloperResourcesIcon
    }
  ]
] satisfies EditorToolbarItem<typeof customHandlers>[][]
</script>

<template>
  <B24Editor
    v-slot="{ editor }"
    v-model="value"
    :extensions="[ImageUpload]"
    :handlers="customHandlers"
    content-type="markdown"
    :b24ui="{ base: 'p-8 sm:px-16' }"
    class="w-full min-h-74"
  >
    <B24EditorToolbar
      :editor="editor"
      :items="items"
      class="border-b border-muted bg-default py-2 px-8 sm:px-16 overflow-x-auto"
    />
  </B24Editor>
</template>
Learn more about creating custom extensions in the TipTap documentation.

API

Props

Prop Default Type
as'div'any

The element or component this component should render as.

modelValuenull | string | JSONContent | JSONContent[]
contentType "markdown" | "json" | "html"

The content type the content is provided as. When not specified, it's automatically inferred: strings are treated as 'html', objects as 'json'.

starterKittrueboolean | Partial<StarterKitOptions>

The starter kit options to configure the editor. Set to false for a plain-text editor: keeps the essential nodes (paragraph, text, history) and disables all rich-text formatting.

placeholder{ showOnlyWhenEditable: false, showOnlyCurrent: true, mode: 'everyLine' } string | Partial<PlaceholderOptions> & { mode?: "firstLine" | "everyLine" | undefined; }

The placeholder text to show in empty paragraphs. Can be a string or PlaceholderOptions from @tiptap/extension-placeholder.

markdown{ markedOptions: { gfm: true } } Partial<MarkdownExtensionOptions>

The markdown extension options to configure markdown parsing and serialization.

imagetrueboolean | Partial<ImageOptions>

The image extension options to configure image handling. Set to false to disable the extension.

mentiontrueboolean | Partial<Omit<MentionOptions<any, MentionNodeAttrs>, "suggestion" | "suggestions">>

The mention extension options to configure mention handling. Set to false to disable the extension. The suggestion and suggestions options are omitted as they are managed by the EditorMentionMenu component.

handlers H

Custom item handlers to override or extend the default handlers. These handlers are provided to all child components (toolbar, suggestion menu, etc.).

extensions Extensions

The extensions to use

injectCSSboolean

Whether to inject base CSS styles

injectNonce string

A nonce to use for CSP while injecting styles

autofocus null | number | false | true | "start" | "end" | "all"

The editor's initial focus position

editableboolean

Whether the editor is editable

textDirection "ltr" | "rtl" | "auto"

The default text direction for all content in the editor. When set to 'ltr' or 'rtl', all nodes will have the corresponding dir attribute. When set to 'auto', the dir attribute will be set based on content detection. When undefined, no dir attribute will be added.

editorProps EditorProps<any>

The editor's props

parseOptionsParseOptions
coreExtensionOptions { clipboardTextSerializer?: { blockSeparator?: string | undefined; } | undefined; tabindex?: { value?: string | undefined; } | undefined; delete?: { async?: boolean | undefined; filterTransaction?: ((transaction: Transaction) => boolean) | undefined; } | undefined; }

The editor's core extension options

enableInputRules false | true | (string | AnyExtension)[]

Whether to enable input rules behavior

enablePasteRules false | true | (string | AnyExtension)[]

Whether to enable paste rules behavior

enableCoreExtensionsboolean | Partial<Record<"editable" | "textDirection" | "clipboardTextSerializer" | "commands" | "focusEvents" | "keymap" | "tabindex" | "drop" | "paste" | "delete", false>>

Determines whether core extensions are enabled.

If set to false, all core extensions will be disabled. To disable specific core extensions, provide an object where the keys are the extension names and the values are false. Extensions not listed in the object will remain enabled.

enableContentCheckboolean

If true, the editor will check the content for errors on initialization. Emitting the contentError event if the content is invalid. Which can be used to show a warning or error message to the user.

emitContentErrorboolean

If true, the editor will emit the contentError event if invalid content is encountered but enableContentCheck is false. This lets you preserve the invalid editor content while still showing a warning or error message to the user.

onBeforeCreate (props: { editor: Editor; }): void

Called before the editor is constructed.

onCreate (props: { editor: Editor; }): void

Called after the editor is constructed.

onMount (props: { editor: Editor; }): void

Called when the editor is mounted.

onUnmount (props: { editor: Editor; }): void

Called when the editor is unmounted.

onContentError (props: { editor: Editor; error: Error; disableCollaboration: () => void; }): void

Called when the editor encounters an error while parsing the content. Only enabled if enableContentCheck is true.

onUpdate (props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }): void

Called when the editor's content is updated.

onSelectionUpdate (props: { editor: Editor; transaction: Transaction; }): void

Called when the editor's selection is updated.

onTransaction (props: { editor: Editor; transaction: Transaction; appendedTransactions: Transaction[]; }): void

Called after a transaction is applied to the editor.

onFocus (props: { editor: Editor; event: FocusEvent; transaction: Transaction; }): void

Called on focus events.

onBlur (props: { editor: Editor; event: FocusEvent; transaction: Transaction; }): void

Called on blur events.

onDestroy (props: void): void

Called when the editor is destroyed.

onPaste (e: ClipboardEvent, slice: Slice): void

Called when content is pasted into the editor.

onDrop (e: DragEvent, slice: Slice, moved: boolean): void

Called when content is dropped into the editor.

onDelete (props: { editor: Editor; deletedRange: Range; newRange: Range; transaction: Transaction; combinedTransform: Transform; partial: boolean; from: number; to: number; } & ({ ...; } | { ...; })): void

Called when content is deleted from the editor.

enableExtensionDispatchTransactionboolean

Whether to enable extension-level dispatching of transactions. If false, extensions cannot define their own dispatchTransaction hook.

b24ui { root?: SlotClass; content?: SlotClass; base?: SlotClass; }

Slots

Slot Type
default{ editor: Editor; handlers: EditorHandlers<H>; }

Emits

Event Type
update:modelValue[value: T]

Expose

When accessing the component via a template ref, you can use the following:

NameType
editorRef<Editor | undefined>
The exposed editor instance is the TipTap Editor API. Check the TipTap documentation for all available methods and properties.

Theme

https://github.com/bitrix24/b24ui/tree/main/src/theme/editor.ts
export default {
  slots: {
    root: '',
    content: 'relative size-full flex-1',
    base: 'text-label w-full outline-none *:my-5 *:first:mt-0 *:last:mb-0 sm:px-8 selection:bg-(--ui-color-design-selection-bg) [&_p]:leading-7 [&_a]:text-(--ui-color-accent-main-primary) [&_a]:border-b [&_a]:border-transparent [&_a]:hover:border-(--ui-color-accent-main-primary) [&_a]:font-(--ui-font-weight-medium) [&_a]:transition-colors [&_a>code]:border-dashed [&_a:hover>code]:border-(--ui-color-accent-soft-element-blue) [&_a:hover>code]:text-(--ui-color-accent-main-primary) [&_a>code]:transition-colors [&_.mention]:text-(--ui-color-accent-main-primary) [&_.mention]:font-(--ui-font-weight-medium) [&_:is(h1,h2,h3,h4,h5,h6)]:text-label [&_:is(h1,h2,h3,h4,h5,h6)]:font-(--ui-font-weight-bold) [&_h1]:text-3xl [&_h2]:text-2xl [&_h3]:text-xl [&_h4]:text-lg [&_h5]:text-md [&_h6]:text-md [&_:is(h1,h2,h3,h4,h5,h6)>code]:border-dashed [&_:is(h1,h2,h3,h4,h5,h6)>code]:font-(--ui-font-weight-bold) [&_h2>code]:text-xl/6 [&_h3>code]:text-lg/5 [&_blockquote]:border-s-4 [&_blockquote]:border-(--ui-color-accent-soft-element-blue) [&_blockquote]:ps-4 [&_blockquote]:italic [&_[data-type=horizontalRule]]:my-8 [&_[data-type=horizontalRule]]:py-2 [&_hr]:border-t [&_hr]:border-(--ui-color-divider-default) [&_pre]:text-sm/6 [&_pre]:border [&_pre]:border-(--ui-color-design-tinted-na-stroke) [&_pre]:bg-(--ui-color-design-tinted-na-bg) [&_pre]:rounded-md [&_pre]:px-4 [&_pre]:py-3 [&_pre]:whitespace-pre-wrap [&_pre]:wrap-break-word [&_pre]:overflow-x-auto [&_pre_code]:p-0 [&_pre_code]:text-inherit [&_pre_code]:font-inherit [&_pre_code]:rounded-none [&_pre_code]:inline [&_pre_code]:border-0 [&_pre_code]:bg-transparent [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-[family-name:var(--ui-font-family-system-mono)] [&_code]:font-(--ui-font-weight-medium) [&_code]:rounded-md [&_code]:inline-block [&_code]:border [&_code]:border-(--ui-color-design-tinted-na-stroke) [&_code]:text-label [&_code]:bg-(--ui-color-design-tinted-na-bg) [&_:is(ul,ol)]:ps-6 [&_ul]:list-disc [&_ul]:marker:text-(--ui-color-accent-soft-element-blue) [&_ol]:list-decimal [&_ol]:marker:text-label [&_li]:my-1.5 [&_li]:ps-1.5 [&_img]:rounded-md [&_img]:block [&_img]:max-w-full [&_img.ProseMirror-selectednode]:outline-2 [&_img.ProseMirror-selectednode]:outline-(--ui-color-accent-main-primary) [&_.ProseMirror-selectednode:not(img):not(pre):not([data-node-view-wrapper])]:bg-(--ui-color-design-selection-bg)'
  },
  variants: {
    placeholderMode: {
      firstLine: {
        base: '[&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:content-[attr(data-placeholder)] [&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:text-description [&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:float-start [&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:h-0 [&_:is(p,h1,h2,h3,h4,h5,h6).is-editor-empty:first-child]:before:pointer-events-none'
      },
      everyLine: {
        base: '[&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:content-[attr(data-placeholder)] [&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:text-description [&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:float-start [&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:h-0 [&_:is(p,h1,h2,h3,h4,h5,h6).is-empty]:before:pointer-events-none'
      }
    }
  },
  defaultVariants: {
    placeholderMode: 'everyLine'
  }
}