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.
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.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)
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>
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>
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>
includeChildren to true:<template>
<B24Editor :placeholder="{ placeholder: 'Start writing...', includeChildren: true }" />
</template>
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>
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.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:
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>
Examples
With toolbar
You can use the EditorToolbar component to add a fixed, bubble, or floating toolbar to the Editor with common formatting actions.
With drag handle
You can use the EditorDragHandle component to add a draggable handle for reordering blocks.
With suggestion menu
You can use the EditorSuggestionMenu component to add slash commands for quick formatting and insertions.
With mention menu
You can use the EditorMentionMenu component to add @ mentions for tagging users or entities.
With emoji menu
You can use the EditorEmojiMenu component to add emoji picker support.
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.
- Create a Vue component that uses the FileUpload component:
- Create a custom TipTap extension to register the node:
- Use the custom extension in the Editor:
API
Props
Slots
Emits
Expose
When accessing the component via a template ref, you can use the following:
Theme
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'
}
}