Component

Document editor

Document editor

A client-only WYSIWYG editing surface with structured JSON output, app-owned AI rewrite suggestions, media insertion, video embeds, and optional Yjs collaboration.

Demo

AI-ready editing workspace

A Pinia setup store loads and saves structured document JSON, emulates an AI rewrite endpoint, and opens DomMediaBrowser when the editor image tool requests media.

Untitled document

Draft copy for the July product launch.

Loading document...
{}
{}

Insert image

Choose an image from the same media browser used by upload and picker workflows.

Editor image library

No items selected

No media yet

Drop an image here or navigate to another folder.

Select an image file to insert it into the document.

Form

Textarea-style v-model

A compact form field setup uses DomWysiwyg with a normal v-model binding, a reduced WYSIWYG toolbar, and no collaboration, rewrite, media, or external store wiring.

The editor mounts in the browser.

Edit link

Add a destination URL for the selected text.

Embed video

Paste a YouTube, Vimeo, or iframe-safe video URL.

Current v-model

{
"title":"July launch note",
"body":{
"type":"doc",
"content":[...]3 items
}
}

Submitted payload

{}

Extend

Toolbar tools and document behavior

Custom toolbar tools

Pass customTools records. Each command receives the editor instance, current selection, and helpers such as requestRewrite.

Typed editor behavior

Use extensions or configureExtensions when a feature belongs inside the document model, such as mentions, embeds, or custom marks.

App-owned popups

Menus for people, assets, records, and commands stay in the app. The editor supplies the trigger context; the owner renders and commits the selected item.

Register a custom toolbar tool, open an app-owned user picker, and insert a mention into the active editor.

The editor mounts in the browser.

Edit link

Add a destination URL for the selected text.

Embed video

Paste a YouTube, Vimeo, or iframe-safe video URL.

Custom tools

App-owned

Toolbar tools are plain records. A command gets the editor instance and helper methods; the owner can still listen to tool-action.

Last tool: None yet

custom-toolbar-tool.jsjs
mention-extension.jsjs

Boundary

Editor experience, not document output

Client-only runtime

DomDocumentEditor dynamically imports the Tiptap runtime after mount. The docs shell can SSR, but the editing canvas belongs to the browser.

Structured persistence

The editor emits Tiptap JSON, HTML, and plain text in the change payload. Store JSON for editing and render public document output through a separate renderer.

App-owned integrations

AI rewrite and media picking are events. Apps keep model calls, media permissions, and storage rules behind their own API boundaries.

Usage

Client editor integration

The component is safe to place on an SSR page because the editor packages are loaded from onMounted. Use the page for an authenticated editing workflow and keep SEO rendering separate.

document-editor.vuevue

AI

Suggestion contract

When the user asks for a rewrite, the component emits the selected text, JSON slice, document range, and a respond callback. The owner can call an AI endpoint, then pass back a suggestion.

Pending suggestions mark the original range inline and render a word-level diff in the review rail. Accepting replaces the current marked range; rejecting removes the mark.

document-editor-suggestion.jsjs

Collaboration

Yjs adapter

Pass a Yjs document or fragment when the app wants true multi-user editing. The component wires Tiptap's collaboration extension and, when a provider is present, collaboration carets.

The AI can be represented as a collaborator in the header today and as a provider awareness user later while it is actively proposing changes.

document-editor-yjs.jsjs

Demo API

Store and API boundaries

useDocumentEditorDemoStore.jsjs
documentEditorMockApi.jsjs

Reference

Component API

Props

Control props

NameTypeTSDefaultDescription
modelValueobject | stringRecord<string, unknown>() => …Controlled editor content. JSON is preferred; HTML strings are accepted for simple imports.
placeholderstringstring'Write something useful...'Placeholder shown inside empty blocks.
chrome'document' | 'field' | 'none' | falsestring'document'Render the full document workspace, form-field chrome, or no outer field chrome.
output'json' | 'html'string'json'Value emitted through update:modelValue.
editablebooleanbooleantrueAllow editing and toolbar commands.
titlestringstring'Document editor'Accessible editor title.
statusstringstring''Optional save or collaboration status label.
toolsarrayArray<unknown>() => …Built-in toolbar tool ids to show.
customToolsarrayArray<unknown>[]Additional toolbar actions emitted through tool-action.
suggestionsarrayArray<unknown>[]Externally supplied rewrite suggestions.
collaboratorsarrayArray<unknown>[]Human, AI, or system collaborators shown in the editor header.
extensionsarrayArray<unknown>[]Additional Tiptap extensions appended to the built-in editor extension list.
configureExtensionsfunctionFunctionOptional hook that receives the default extension list and Tiptap runtime modules, then returns the final extension list.
collaborationobjectRecord<string, unknown>nullOptional Yjs collaboration adapter with document/fragment, provider, field, and user.
rewriteProviderfunctionFunctionOptional async function that returns a rewrite suggestion for the current selection.
aiUserobjectRecord<string, unknown>() => …AI author metadata used for locally created rewrite suggestions.
showSuggestionRailbooleanbooleantrueShow the review rail for pending AI suggestions.
showToolbarbooleanbooleantrueShow the WYSIWYG toolbar. Slash commands remain available when enabled.
showSlashMenubooleanbooleantrueOpen the block command menu when the user types / at the start of a block.
autofocusbooleanbooleanfalseFocus the editor when the client runtime mounts.
minHeightstringstring'34rem'Minimum height for the editable document canvas.

Field props

NameTypeTSDefaultDescription
idstringstring''Optional ID override. By default parent forms derive the input ID from the field path using underscores.
namestringstring''Local field name. Parent forms derive the full field path and native HTML name from the form hierarchy.
labelstringstring''Visible field label.
descriptionstringstring''Optional helper copy below the field.
requiredbooleanbooleanfalseMark the field as required.
disabledbooleanbooleanfalseDisable field interaction.
readOnlybooleanbooleanfalseShow the value but prevent editing.
invalidbooleanbooleanfalseMark the field invalid.
errors
[
	{
		name: "Validation name",
		message: "Error message",
	}
]
array | object | stringArray<ErrorsItem
type ErrorsItem = {
	name?: string; // Name
	message?: string; // Message
};
>
[]Validation errors for this field.
visiblebooleanbooleantrueShow or hide the field.
validatorsarrayArray<unknown>[]Validators attached to this field. Use functions in Vue code, or serializable records such as { name: "minLength", props: { min: 2 } } in generated schemas.
validateOnBlurbooleanbooleantrueRun validators when the field loses focus.

Auto-generated from Document editor.props and inline _edit hints.

Events

NamePayloadDescription
@update:modelValueRecord<string, unknown> | stringFired with JSON or HTML content depending on the output prop.
@changeDocumentEditorChangePayloadFired with JSON, HTML, and plain text whenever the editor changes.
@selection-changeDocumentEditorSelectionPayloadFired when the editor selection changes.
@rewrite-requestDocumentEditorRewriteRequestFired when the user asks AI to rewrite the selected range.
@suggestion-createDocumentEditorSuggestionFired after a rewrite suggestion is added to the review rail.
@suggestion-acceptDocumentEditorSuggestionFired after a suggestion is accepted.
@suggestion-rejectDocumentEditorSuggestionFired after a suggestion is rejected.
@media-request({ editor })Fired when the image tool should open an app-owned media picker.
@media-insertDocumentEditorMediaPayloadFired after insertImage inserts a media-backed image node.
@video-insertDocumentEditorVideoPayloadFired after a video embed node is inserted.
@tool-action({ tool, editor })Fired when a custom toolbar tool is activated.
@ready({ editor })Fired after the browser-only editor runtime mounts.
@runtime-errorErrorFired when the editor runtime cannot mount.
@focusFocusEventFired when the editable document receives focus.
@blurFocusEventFired when the editable document loses focus.

Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.

Slots

NameScopeDescription
#toolbar-start{ editor, active }Add controls before the built-in toolbar groups.
#toolbar-end{ editor, active }Add controls after the built-in toolbar groups.
#suggestion{ suggestion, accept, reject }Replace a suggestion review card.
#empty-suggestionsReplace the empty suggestion rail state.

Props

Control props

NameTypeTSDefaultDescription
modelValueobject | stringRecord<string, unknown>() => …Controlled editor content. JSON is preferred; HTML strings are accepted for simple imports.
output'json' | 'html'string'json'Value emitted through update:modelValue.
editablebooleanbooleantrueAllow editing and toolbar commands.
titlestringstring''Accessible editor title. Defaults to the field label.
statusstringstring''Optional save or validation status label.
toolsarrayArray<unknown>() => …Built-in toolbar tool ids to show.
customToolsarrayArray<unknown>[]Additional toolbar actions emitted through tool-action.
showToolbarbooleanbooleantrueShow the WYSIWYG toolbar.
showSlashMenubooleanbooleantrueOpen the block command menu when the user types / at the start of a block.
minHeightstringstring'16rem'Minimum height for the editable field.

Field props

NameTypeTSDefaultDescription
idstringstring''Optional ID override. By default parent forms derive the input ID from the field path using underscores.
namestringstring''Local field name. Parent forms derive the full field path and native HTML name from the form hierarchy.
labelstringstring''Visible field label.
descriptionstringstring''Optional helper copy below the field.
placeholderstringstring''Placeholder shown when the control is empty.
requiredbooleanbooleanfalseMark the field as required.
disabledbooleanbooleanfalseDisable field interaction.
readOnlybooleanbooleanfalseShow the value but prevent editing.
invalidbooleanbooleanfalseMark the field invalid.
errors
[
	{
		name: "Validation name",
		message: "Error message",
	}
]
array | object | stringArray<ErrorsItem
type ErrorsItem = {
	name?: string; // Name
	message?: string; // Message
};
>
[]Validation errors for this field.
visiblebooleanbooleantrueShow or hide the field.
validatorsarrayArray<unknown>[]Validators attached to this field. Use functions in Vue code, or serializable records such as { name: "minLength", props: { min: 2 } } in generated schemas.
validateOnBlurbooleanbooleantrueRun validators when the field loses focus.
chrome'field' | 'none' | falsestring'field'Render default field chrome, or hide chrome while keeping form state wiring.

Auto-generated from WYSIWYG field.props and inline _edit hints.

Events

NamePayloadDescription
@update:modelValueRecord<string, unknown> | stringFired with JSON or HTML content depending on the output prop.
@changeDocumentEditorChangePayloadFired with JSON, HTML, and plain text whenever the editor changes.
@selection-changeDocumentEditorSelectionPayloadFired when the editor selection changes.
@tool-action({ tool, editor })Fired when a custom toolbar tool is activated.
@ready({ editor })Fired after the browser-only editor runtime mounts.
@runtime-errorErrorFired when the editor runtime cannot mount.
@focusFocusEventFired when the editable document receives focus.
@blurFocusEventFired when the editable document loses focus.

Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.