Vue
The @domternal/vue package provides composable components and hooks that wrap the headless editor with Vue-native APIs. Works with Vue 3.3+ and uses the Composition API throughout.
Installation
Section titled “Installation”pnpm add @domternal/core @domternal/theme @domternal/vueImport the theme in your main entry file or component:
import '@domternal/theme';Quick start
Section titled “Quick start”The recommended pattern uses the composable Domternal component with namespaced subcomponents:
<script setup lang="ts">import { Domternal } from '@domternal/vue';import { StarterKit, BubbleMenu } from '@domternal/core';
const extensions = [StarterKit, BubbleMenu];</script>
<template> <Domternal :extensions="extensions" content="<p>Hello from Vue!</p>"> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.BubbleMenu :contexts="{ text: ['bold', 'italic', 'underline'] }" /> </Domternal></template>Domternal creates the editor and provides it to all subcomponents via Vue’s provide/inject. No need to pass editor props manually.
Two approaches
Section titled “Two approaches”Composable pattern (recommended)
Section titled “Composable pattern (recommended)”The Domternal component handles editor creation and context internally. Subcomponents access the editor automatically:
<template> <Domternal :extensions="extensions" :content="content"> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.BubbleMenu /> <Domternal.EmojiPicker :emojis="emojis" /> </Domternal></template>Composable hook pattern (full control)
Section titled “Composable hook pattern (full control)”Use useEditor when you need direct access to the editor instance or custom rendering:
<script setup lang="ts">import { useEditor, provideEditor, DomternalToolbar } from '@domternal/vue';import { StarterKit } from '@domternal/core';
const { editor, editorRef } = useEditor({ extensions: [StarterKit], content: '<p>Hello</p>',});
provideEditor(editor);</script>
<template> <DomternalToolbar v-if="editor" /> <div class="dm-editor"> <div ref="editorRef" /> </div></template>Composables
Section titled “Composables”useEditor
Section titled “useEditor”Core composable for creating and managing an editor instance.
const { editor, editorRef } = useEditor(options);Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
extensions | AnyExtension[] | [] | Extensions to load. Default extensions (Document, Paragraph, Text, BaseKeymap, History) are always included. |
content | Content | '' | Initial content (HTML string or JSON). Reactive: changing this syncs the editor. |
editable | boolean | true | Whether the editor is editable. Reactive. |
autofocus | FocusPosition | false | Focus on mount: true, 'start', 'end', 'all', or a position number. |
outputFormat | 'html' | 'json' | 'html' | Format used for content comparison when syncing. |
immediatelyRender | boolean | false | Set to true to create the editor synchronously during setup instead of waiting for onMounted. |
onCreate | (editor: Editor) => void | - | Called when the editor is created. |
onUpdate | (props: { editor: Editor }) => void | - | Called when the document content changes. |
onSelectionChange | (props: { editor: Editor }) => void | - | Called when the selection changes without content change. |
onFocus | (props: { editor: Editor; event: FocusEvent }) => void | - | Called when the editor gains focus. |
onBlur | (props: { editor: Editor; event: FocusEvent }) => void | - | Called when the editor loses focus. |
onDestroy | () => void | - | Called before the editor is destroyed. |
Return value
Section titled “Return value”| Property | Type | Description |
|---|---|---|
editor | ShallowRef<Editor | null> | The editor instance. Null until onMounted (or immediately if immediatelyRender is true). |
editorRef | Ref<HTMLDivElement | undefined> | Template ref to attach to the editor mount point. |
SSR behavior
Section titled “SSR behavior”By default, useEditor creates the editor in onMounted, which never runs on the server. This makes it SSR-safe out of the box. Set immediatelyRender: true only when you’re certain your code doesn’t run on the server and you need synchronous editor creation.
Extension changes
Section titled “Extension changes”When the extensions array reference changes, the editor is destroyed and recreated. Content is preserved automatically via JSON serialization.
useEditorState
Section titled “useEditorState”Subscribe to editor state changes. Two overloads for different use cases.
Full state (simple)
Section titled “Full state (simple)”Returns all state properties as individual refs. Updates when any state changes:
const { htmlContent, jsonContent, isEmpty, isFocused, isEditable } = useEditorState(editor);| Property | Type | Description |
|---|---|---|
htmlContent | Ref<string> | Current document as HTML. |
jsonContent | Ref<JSONContent | null> | Current document as JSON. |
isEmpty | Ref<boolean> | Whether the document is empty. |
isFocused | Ref<boolean> | Whether the editor has focus. |
isEditable | Ref<boolean> | Whether the editor is editable. |
Selector (granular)
Section titled “Selector (granular)”Returns a computed ref with a derived value. Only recomputes when the editor state changes:
const isBold = useEditorState(editor, (ed) => ed.isActive('bold'));const wordCount = useEditorState(editor, (ed) => ed.state.doc.textContent.split(/\s+/).filter(Boolean).length);The selector receives the full Editor instance and returns T | undefined. The return is undefined when the editor is null or destroyed.
useCurrentEditor
Section titled “useCurrentEditor”Access the editor instance from the nearest Domternal or provideEditor ancestor:
<script setup lang="ts">import { useCurrentEditor } from '@domternal/vue';
const { editor } = useCurrentEditor();</script>
<template> <button v-if="editor" @click="editor.chain().focus().toggleBold().run()"> Bold </button></template>Returns { editor: ShallowRef<Editor | null> }. Returns a ref with null if used outside a provider.
provideEditor
Section titled “provideEditor”Provides the editor instance to all descendant components via Vue’s provide/inject:
import { useEditor, provideEditor } from '@domternal/vue';
const { editor } = useEditor({ extensions, content });provideEditor(editor);This is called automatically inside Domternal and DomternalEditor. You only need to call it manually when using useEditor directly, especially when using VueNodeViewRenderer (see Custom node views).
provideEditor also captures the Vue app context (including all ancestor provide values) and stores it for node view components. This is how provide/inject works across the ProseMirror boundary.
Components
Section titled “Components”Domternal
Section titled “Domternal”Composable root that creates an editor and provides it via inject. Accepts all useEditor options as props.
All useEditor options are accepted as props:
| Prop | Type | Default | Description |
|---|---|---|---|
extensions | AnyExtension[] | [] | Extensions to load. |
content | Content | '' | Initial content (HTML string or JSON). |
editable | boolean | true | Whether the editor is editable. Reactive. |
autofocus | FocusPosition | false | Focus on mount. |
outputFormat | 'html' | 'json' | 'html' | Format for content comparison. |
immediatelyRender | boolean | false | Create editor synchronously. |
onCreate | Function | - | Called when editor is created. |
onUpdate | Function | - | Called on content changes. |
onSelectionChange | Function | - | Called on selection changes. |
onFocus | Function | - | Called on focus. |
onBlur | Function | - | Called on blur. |
onDestroy | Function | - | Called before destroy. |
Subcomponents
Section titled “Subcomponents”| Component | Props | Description |
|---|---|---|
Domternal.Content | class?: string | Renders the editor content area. Mounts the ProseMirror DOM. |
Domternal.Loading | default slot | Renders slot content only while editor is not ready (SSR loading state). |
Domternal.Toolbar | icons?: IconSet, layout?: ToolbarLayoutEntry[] | Auto-rendering toolbar with optional custom icons and layout. |
Domternal.BubbleMenu | items?, contexts?, shouldShow?, placement?, offset?, updateDelay? | Context-aware bubble menu. See DomternalBubbleMenu. |
Domternal.FloatingMenu | shouldShow?, offset? | Floating menu on empty lines. See DomternalFloatingMenu. |
Domternal.EmojiPicker | emojis: EmojiPickerItem[] | Searchable emoji picker. See DomternalEmojiPicker. |
All subcomponents automatically use the editor from context. No editor prop needed.
DomternalEditor
Section titled “DomternalEditor”All-in-one editor with integrated state management, context provider, and v-model support. Alternative to the composable pattern.
<script setup lang="ts">import { ref } from 'vue';import { DomternalEditor, DomternalToolbar } from '@domternal/vue';import { StarterKit } from '@domternal/core';
const content = ref('<p>Hello</p>');const editorRef = ref();</script>
<template> <DomternalEditor ref="editorRef" :extensions="[StarterKit]" v-model="content" > <DomternalToolbar /> </DomternalEditor></template>All useEditor options plus:
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | CSS class for the .dm-editor wrapper. |
outputFormat | 'html' | 'json' | 'html' | Format used by v-model and content comparison. |
modelValue | Content | - | v-model binding. When set, two-way syncs editor content. |
| Event | Payload | Description |
|---|---|---|
update:modelValue | string | JSONContent | Emitted on content changes. Format depends on outputFormat. |
Template ref
Section titled “Template ref”Access the editor instance and state via template ref:
| Property | Type | Description |
|---|---|---|
editor | ShallowRef<Editor | null> | The editor instance. |
htmlContent | Ref<string> | Current HTML content. |
jsonContent | Ref<JSONContent | null> | Current JSON content. |
isEmpty | Ref<boolean> | Whether the document is empty. |
isFocused | Ref<boolean> | Whether the editor has focus. |
v-model
Section titled “v-model”<script setup lang="ts">import { ref } from 'vue';import { DomternalEditor } from '@domternal/vue';import { StarterKit } from '@domternal/core';
const content = ref('<p>Hello</p>');</script>
<template> <DomternalEditor :extensions="[StarterKit]" v-model="content" /> <pre>{{ content }}</pre></template>By default, v-model syncs HTML strings. Set outputFormat="json" to sync JSON instead:
<DomternalEditor :extensions="[StarterKit]" v-model="content" output-format="json"/>DomternalToolbar
Section titled “DomternalToolbar”Auto-renders toolbar buttons, dropdowns, separators, and keyboard navigation based on the editor’s extensions.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | from context | The editor instance. Falls back to context if omitted. |
icons | IconSet | undefined | Custom icon set. Falls back to built-in defaultIcons. |
layout | ToolbarLayoutEntry[] | undefined | Custom layout to reorder, group, or filter toolbar items. |
Layout
Section titled “Layout”By default, the toolbar renders all items grouped by their group property. Use layout to customize:
<script setup lang="ts">import { Domternal } from '@domternal/vue';import { StarterKit, BubbleMenu } from '@domternal/core';import type { ToolbarLayoutEntry } from '@domternal/core';
const extensions = [StarterKit, BubbleMenu];
const layout: ToolbarLayoutEntry[] = [ 'bold', 'italic', 'underline', '|', 'heading', '|', 'bulletList', 'orderedList',];</script>
<template> <Domternal :extensions="extensions" content="<p>Hello</p>"> <Domternal.Toolbar :layout="layout" /> <Domternal.Content /> </Domternal></template>Keyboard navigation
Section titled “Keyboard navigation”| Key | Action |
|---|---|
ArrowRight | Focus next button |
ArrowLeft | Focus previous button |
ArrowDown | Open dropdown / focus next dropdown item |
ArrowUp | Focus previous dropdown item |
Home | Focus first button |
End | Focus last button |
Escape | Close open dropdown |
DomternalBubbleMenu
Section titled “DomternalBubbleMenu”An inline formatting toolbar that appears when the user selects text.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | from context | The editor instance. |
items | string[] | - | Fixed list of item names to show. Use `' |
contexts | Record<string, string[] | true | null> | - | Context-aware items. Key is a node type name or 'text'/'table'. Use `' |
shouldShow | (props: { editor, view, state, from, to }) => boolean | - | Custom predicate to control visibility. |
placement | 'top' | 'bottom' | 'top' | Menu placement relative to the selection. |
offset | number | 8 | Pixel offset from the selection. |
updateDelay | number | 0 | Delay in milliseconds before updating position. |
Context-aware menus
Section titled “Context-aware menus”Show different buttons depending on what the user selected:
<Domternal.BubbleMenu :contexts="{ text: ['bold', 'italic', 'underline', '|', 'link'], heading: ['bold', 'italic', '|', 'link'], codeBlock: null, image: ['imageFloatLeft', 'imageFloatCenter', 'imageFloatRight', '|', 'deleteImage'], }"/>text- Default for text selectionsnull- Hide the menu in that contexttrue- Show all valid items for that context
Default behavior
Section titled “Default behavior”When neither items nor contexts is provided:
- Shows
bold,italic,underlinebuttons on text selection - Automatically shows image/node-specific items when a node with registered
bubbleMenuitems is selected (e.g., image float/delete buttons) - Hidden inside table cells (tables have their own cell toolbar)
Custom content
Section titled “Custom content”Add custom buttons via the default slot:
<Domternal.BubbleMenu :contexts="{ text: ['bold', 'italic'] }"> <button @click="handleCustomAction">Custom</button></Domternal.BubbleMenu>DomternalFloatingMenu
Section titled “DomternalFloatingMenu”A menu that appears on empty lines, allowing users to insert block-level content.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | from context | The editor instance. |
shouldShow | (props: { editor, view, state }) => boolean | - | Custom predicate to control visibility. Default: shows on empty paragraphs. |
offset | number | 0 | Pixel offset from the cursor position. |
<script setup lang="ts">import { useCurrentEditor } from '@domternal/vue';
const { editor } = useCurrentEditor();</script>
<template> <Domternal.FloatingMenu> <button @click="editor?.chain().focus().setHeading({ level: 1 }).run()">H1</button> <button @click="editor?.chain().focus().toggleBulletList().run()">List</button> <button @click="editor?.chain().focus().setHorizontalRule().run()">Divider</button> </Domternal.FloatingMenu></template>DomternalEmojiPicker
Section titled “DomternalEmojiPicker”A searchable emoji picker panel that opens from the toolbar emoji button.
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | from context | The editor instance. |
emojis | EmojiPickerItem[] | required | Array of emoji definitions with emoji, name, and group properties. Import the type from @domternal/vue. |
<script setup lang="ts">import { Domternal } from '@domternal/vue';import { StarterKit } from '@domternal/core';import { Emoji, emojis } from '@domternal/extension-emoji';import type { EmojiPickerItem } from '@domternal/vue';
const extensions = [StarterKit, Emoji];</script>
<template> <Domternal :extensions="extensions" content="<p>Hello</p>"> <Domternal.Toolbar /> <Domternal.Content /> <Domternal.EmojiPicker :emojis="emojis" /> </Domternal></template>The picker opens when the user clicks the emoji toolbar button. It includes search, category tabs, frequently used section, grid keyboard navigation, and smooth scrolling.
EditorContent
Section titled “EditorContent”Low-level component that mounts the ProseMirror DOM into a div. Used with the useEditor composable when you need full control over the layout.
<script setup lang="ts">import { useEditor, EditorContent } from '@domternal/vue';import { StarterKit } from '@domternal/core';
const { editor } = useEditor({ extensions: [StarterKit], content: '<p>Hello</p>',});</script>
<template> <EditorContent :editor="editor" class="dm-editor" /></template>| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | null | The editor instance to render. |
class | string | - | CSS class on the container. |
Custom node views
Section titled “Custom node views”Render Vue components inside the editor document. This lets you build interactive, editable nodes with full Vue lifecycle support and provide/inject access.
VueNodeViewRenderer
Section titled “VueNodeViewRenderer”Converts a Vue component into a ProseMirror NodeView constructor:
<script setup lang="ts">import { NodeViewWrapper, NodeViewContent } from '@domternal/vue';import type { VueNodeViewProps } from '@domternal/vue';
const props = defineProps<VueNodeViewProps>();</script>
<template> <NodeViewWrapper :class="`callout callout-${node.attrs['type']}`"> <select :value="node.attrs['type']" @change="updateAttributes({ type: ($event.target as HTMLSelectElement).value })" contenteditable="false" > <option value="info">Info</option> <option value="warning">Warning</option> <option value="error">Error</option> </select> <NodeViewContent /> </NodeViewWrapper></template>import { Node } from '@domternal/core';import { VueNodeViewRenderer } from '@domternal/vue';import CalloutView from './CalloutView.vue';
export const Callout = Node.create({ name: 'callout', group: 'block', content: 'inline*',
addAttributes() { return { type: { default: 'info' } }; },
parseHTML() { return [{ tag: 'div[data-callout]' }]; },
renderHTML({ node, HTMLAttributes }) { return ['div', { ...HTMLAttributes, 'data-callout': node.attrs['type'] }, 0]; },
addNodeView() { return VueNodeViewRenderer(CalloutView); },});VueNodeViewProps
Section titled “VueNodeViewProps”| Prop | Type | Description |
|---|---|---|
editor | Editor | The editor instance. |
node | PMNode | The ProseMirror node being rendered. |
selected | boolean | Whether this node is selected via NodeSelection. |
getPos | () => number | undefined | Get the document position of this node. |
updateAttributes | (attrs) => void | Update the node’s attributes. |
deleteNode | () => void | Delete this node from the document. |
extension | { name, options } | The extension that created this node view. |
decorations | unknown[] | ProseMirror decorations applied to this node. |
Options
Section titled “Options”| Option | Type | Default | Description |
|---|---|---|---|
as | string | 'div' / 'span' | Wrapper element tag. Defaults to 'div' for block nodes, 'span' for inline. |
className | string | - | CSS class on the wrapper element. |
contentDOMElement | string | null | 'div' | Tag for the content DOM element. Set to null for nodes with no editable content. |
App context forwarding
Section titled “App context forwarding”VueNodeViewRenderer forwards the Vue app context (provide/inject chain) from the parent component tree to node view components. This happens automatically when you use Domternal or call provideEditor() in the setup function.
Inside a node view component, you can use useCurrentEditor() and any other injected values from ancestor components:
<script setup lang="ts">import { useCurrentEditor } from '@domternal/vue';
const { editor } = useCurrentEditor();</script>NodeViewWrapper
Section titled “NodeViewWrapper”Container component for custom node views. Handles drag events and marks the element as a node view wrapper.
<NodeViewWrapper as="section" class="my-node"> <!-- Node view UI --></NodeViewWrapper>| Prop | Type | Default | Description |
|---|---|---|---|
as | string | 'div' | HTML element type to render. |
Plus all standard HTML element attributes via v-bind="$attrs".
NodeViewContent
Section titled “NodeViewContent”Placeholder for editable nested content. ProseMirror manages the DOM inside this element.
<NodeViewContent as="p" class="my-content" />| Prop | Type | Default | Description |
|---|---|---|---|
as | string | 'div' | HTML element type to render. |
Plus all standard HTML element attributes via v-bind="$attrs".
useVueNodeView
Section titled “useVueNodeView”Access node view context inside NodeViewWrapper and NodeViewContent:
import { useVueNodeView } from '@domternal/vue';
const { onDragStart, nodeViewContentRef } = useVueNodeView();// onDragStart: drag event handler// nodeViewContentRef: ref callback for content DOMSSR (Nuxt)
Section titled “SSR (Nuxt)”Vue’s onMounted never runs on the server, so useEditor is SSR-safe by default. The editor is null during server-side rendering and created after mount on the client.
Use Domternal.Loading to show a placeholder while the editor initializes:
<Domternal :extensions="extensions" content="<p>Hello</p>"> <Domternal.Loading> <div>Loading editor...</div> </Domternal.Loading> <Domternal.Toolbar /> <Domternal.Content /></Domternal>Domternal.Loading renders its slot content only while the editor is null. Once the editor is created on the client, the loading content disappears and the toolbar and content are rendered.
For SSR content generation without a browser, use the core SSR helpers:
import { generateHTML, generateJSON, generateText } from '@domternal/vue';// These are re-exported from @domternal/core for convenienceDark mode
Section titled “Dark mode”Apply the theme class to a parent element:
<!-- Always dark --><div class="dm-theme-dark"> <Domternal :extensions="extensions"> <Domternal.Toolbar /> <Domternal.Content /> </Domternal></div>
<!-- Follow system preference --><div class="dm-theme-auto"> <Domternal :extensions="extensions"> <Domternal.Toolbar /> <Domternal.Content /> </Domternal></div>Toggle at runtime:
function toggleTheme() { document.body.classList.toggle('dm-theme-dark');}Custom icons
Section titled “Custom icons”Replace the built-in Phosphor icons with your own:
<script setup lang="ts">import { Domternal } from '@domternal/vue';import { defaultIcons } from '@domternal/core';import type { IconSet } from '@domternal/core';
const customIcons: IconSet = { textB: '<svg>...</svg>', textItalic: '<svg>...</svg>',};</script>
<template> <Domternal.Toolbar :icons="customIcons" /></template>When icons is provided, the component uses only that set. To override just a few icons, spread defaultIcons:
import { defaultIcons } from '@domternal/core';
const myIcons = { ...defaultIcons, textB: '<svg><!-- custom bold --></svg>' };Tree-shaking
Section titled “Tree-shaking”Importing Domternal includes all subcomponents (Toolbar, BubbleMenu, FloatingMenu, EmojiPicker) in your bundle. For a smaller bundle, import only what you need:
// Compound - includes all subcomponentsimport { Domternal } from '@domternal/vue';
// Standalone - only what you import ends up in your bundleimport { useEditor, DomternalToolbar, EditorContent } from '@domternal/vue';See Packages & Bundle Size for details.
Component reference
Section titled “Component reference”All components can be imported individually or used as Domternal subcomponents:
// Individual importsimport { useEditor, useEditorState, useCurrentEditor, provideEditor, EDITOR_KEY, Domternal, DomternalEditor, EditorContent, DomternalToolbar, DomternalBubbleMenu, DomternalFloatingMenu, DomternalEmojiPicker, VueNodeViewRenderer, NodeViewWrapper, NodeViewContent, useVueNodeView, DEFAULT_EXTENSIONS,} from '@domternal/vue';Vue-specific types:
import type { UseEditorOptions, EditorState, VueNodeViewProps, VueNodeViewRendererOptions, EmojiPickerItem,} from '@domternal/vue';Re-exported types from @domternal/core for convenience:
import type { Content, AnyExtension, FocusPosition, JSONContent } from '@domternal/vue';import { Editor } from '@domternal/vue';import { generateHTML, generateJSON, generateText } from '@domternal/vue';Requirements
Section titled “Requirements”- Vue 3.3.0 or later
@domternal/core0.6.0 or later