Component

Document editor

Document editor

A client-only WYSIWYG editing surface with headings, language-aware highlighted code blocks, editable tables, structured JSON output, app-owned AI rewrite suggestions, media insertion, video embeds, and optional Yjs collaboration.

Demo

AI-ready editing workspace

A temporary Yjs room connects every visitor through the app's hardened WebSocket demo endpoint, assigns each person a guest name, and shows live carets and document.content updates alongside the existing mock save, AI rewrite, and media flows.

Untitled document

Connecting to the temporary collaboration room. The room resets every 15 minutes or after its last tab disconnects.

Connecting the temporary document room...

document.content

Updates live from the editor change payload.

{}

Last request

{}

Last response

{}

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.

Long documents

Sticky formatting toolbar

A wrapper-level sticky rule keeps the normal editor toolbar visible while a long document scrolls. The offset can be adjusted when an application has its own fixed header.

1200px

Experiment

AI selection toolbar

The popover, mock provider, review card, and Tiptap decoration extension are isolated in the example folder. They can be removed without unwinding the document editor's built-in editing flow.

Select text inside a paragraph to open Expand, Simplify, and Improve actions. The custom extension highlights the complete paragraph while a separate review component presents the AI response as a diff with accept and reject controls.

Select text inside a paragraph

Choose an AI action from the popover, then compare the suggested paragraph before accepting or rejecting it.

Launch research draft

AI editing experiment

Client only
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.

documentEditorAiSuggestionExtension.jsjs
DocumentEditorAiSuggestionReview.vuevue

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. Code blocks keep their selected language in attrs.language and render a matching language-* class. 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.

This demo mounts a same-origin WebSocket endpoint on the existing Now HTTP server. The server assigns a friendly guest name, relays Yjs document and awareness updates, and keeps one fixed room only in memory. The room is destroyed every 15 minutes or when its final tab disconnects; connected pages reload into a fresh example document after a scheduled reset.

The endpoint is intentionally anonymous, but it rejects cross-origin browser handshakes and applies a 64 KB frame limit, connection and rolling rate caps, a room data budget, heartbeat cleanup, and slow-client backpressure. It remains a throw-away demo boundary: there is no persistence, account identity, private room isolation, or authorization model.

document-editor-yjs.jsjs
documentEditorCollaborationClient.jsjs
documentEditorCollaborationServer.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
ts
[
	{
		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
ts
[
	{
		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.