Rich Text
Headless rich-text editor: a typed document model (schema-declared node and mark types), immutable transactions and commands with undo history, input rules, sanitized HTML paste and serialization, and contenteditable parts.
| Package | @vizejs/ui/rich-text |
| Maturity | stable |
| Own the source | vize lib pull rich-text |
| Requires | collection, context, id, positioner |
| Aliases | wysiwyg, rich text editor, contenteditable editor, prose editor |
| Covers | ProseMirror model, Tiptap, Lexical, Slate, WAI-ARIA textbox |
Usage
import { RichTextRoot, RichText, RichTextBubbleMenu, RichTextContent, RichTextToolbar, RichTextToolbarButton } from "@vizejs/ui/rich-text";
Or copy the source into your project with vize lib pull rich-text (see Source Distribution).
API
RichTextRoot<Schema extends RichTextSchema>
Source: src/families/editor/rich-text/rich-text-root.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
schema (required) |
Schema |
undefined |
Schema declaring node and mark types; documents and commands are typed from it. |
modelValue |
RichTextDoc<Schema> |
undefined |
Controlled document (v-model). undefined selects uncontrolled behavior. |
defaultValue |
RichTextDoc<Schema> |
one empty default block |
Initial uncontrolled document. |
id |
string | null |
undefined |
Consumer-owned editor id. null and undefined select a deterministic fallback. |
editable |
boolean |
true |
Allow editing; false renders read-only content. |
inputRules |
readonly RichTextInputRule[] |
defaultRichTextInputRules(schema) |
Input rules applied to typed text. |
keymap |
RichTextKeymap |
defaultRichTextKeymap(schema) |
Keyboard shortcuts. |
historyDepth |
number |
100 |
Maximum undo depth. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: RichTextDoc<Schema>] |
Fired with the new document after every change (supports v-model). |
transaction |
[transaction: RichTextTransaction] |
Fired for every transaction, including selection-only changes. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
RichTextSlotProps<Schema> |
Editor parts (content, toolbars, bubble menus). Receives the live state and commands. |
Exposed
| Member | Type | Description |
|---|---|---|
state |
RichTextState<Schema> |
Current editor state. |
run |
(command: RichTextCommand) => boolean |
Run a command; returns whether it applied. |
focus |
() => void |
Focus the editable content. |
toHtml |
() => string |
Serialize the document to sanitized HTML. |
RichText<Schema extends RichTextSchema>
Source: src/families/editor/rich-text/rich-text-root.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
schema (required) |
Schema |
undefined |
Schema declaring node and mark types; documents and commands are typed from it. |
modelValue |
RichTextDoc<Schema> |
undefined |
Controlled document (v-model). undefined selects uncontrolled behavior. |
defaultValue |
RichTextDoc<Schema> |
one empty default block |
Initial uncontrolled document. |
id |
string | null |
undefined |
Consumer-owned editor id. null and undefined select a deterministic fallback. |
editable |
boolean |
true |
Allow editing; false renders read-only content. |
inputRules |
readonly RichTextInputRule[] |
defaultRichTextInputRules(schema) |
Input rules applied to typed text. |
keymap |
RichTextKeymap |
defaultRichTextKeymap(schema) |
Keyboard shortcuts. |
historyDepth |
number |
100 |
Maximum undo depth. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[value: RichTextDoc<Schema>] |
Fired with the new document after every change (supports v-model). |
transaction |
[transaction: RichTextTransaction] |
Fired for every transaction, including selection-only changes. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
RichTextSlotProps<Schema> |
Editor parts (content, toolbars, bubble menus). Receives the live state and commands. |
RichTextBubbleMenu
Source: src/families/editor/rich-text/rich-text-bubble-menu.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabel |
string |
"Selection formatting" |
Accessible name of the floating toolbar. |
placement |
Placement |
"top" |
Preferred placement relative to the selection. |
offset |
number |
8 |
Gap between the selection and the menu in CSS pixels. |
open |
boolean |
undefined |
Force visibility; undefined shows the menu for a non-empty selection while editing. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
— | Toolbar buttons for the selection. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
RichTextContent
Source: src/families/editor/rich-text/rich-text-content.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabel |
string |
undefined |
Accessible name of the text box. |
ariaLabelledby |
string |
undefined |
Ids that label the text box. |
ariaDescribedby |
string |
undefined |
Ids that describe the text box. |
placeholder |
string |
undefined |
Placeholder published as data-placeholder (and aria-placeholder) while empty. |
spellcheck |
boolean |
true |
Native spellchecking. |
Exposed
| Member | Type | Description |
|---|---|---|
element |
HTMLDivElement | null |
Rendered editable element. |
focus |
() => void |
Focus the editable element. |
RichTextToolbar
Source: src/families/editor/rich-text/rich-text-toolbar.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
ariaLabel |
string |
"Formatting" |
Accessible name of the toolbar. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
— | RichTextToolbarButton children (and separators or groups). |
Exposed
| Member | Type | Description |
|---|---|---|
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
RichTextToolbarButton
Source: src/families/editor/rich-text/rich-text-toolbar-button.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
command (required) |
RichTextCommand |
undefined |
Command run on activation; the button disables itself while it cannot run. |
active |
(state: RtState) => boolean |
undefined |
Toggle-state predicate; when provided the button publishes aria-pressed. |
ariaLabel |
string |
undefined |
Accessible name when the content is an icon. |
disabled |
boolean |
false |
Force the disabled state. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
RichTextToolbarButtonSlotProps |
Button contents. Receives active and disabled state. |
Behavior
@vizejs/ui/rich-text is a headless, contenteditable rich-text editor. The model (schema, documents,
transforms, transactions, history, commands, input rules, keymap, HTML) is pure TypeScript; the parts
rich-text-root.vue, rich-text-content.vue, rich-text-toolbar.vue, rich-text-toolbar-button.vue, and
rich-text-bubble-menu.vue bind it to the DOM. Every row is proven by the named test in rich-text.test.ts,
rich-text-model.test.ts, rich-text-html.test.ts, or rich-text-ssr.test.ts; typing guarantees live in
rich-text.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| R1 | any | render | rich-text-root.vue wraps rich-text-content.vue: role="textbox", aria-multiline, label, placeholder hooks, contenteditable, deterministic ids, editor hooks on every block |
renders an accessible multiline text box with editor hooks |
| R2 | DOM selection | map to/from model | positions round-trip through text, nested marks, inline leaves, list paths, and element-level points | DOM and model positions round-trip through text, marks, and leaves |
| R3 | editable | beforeinput insertText |
the browser mutation is prevented; the model applies the edit, re-renders, restores the caret, and emits update:modelValue |
beforeinput typing goes through the model, re-renders, and emits v-model |
| R4 | editable | typing, Enter, Backspace, Shift+Enter | input rules, block splitting, joining, and hard breaks run through the keymap | input rules, Enter, Backspace, and line breaks follow the keymap |
| R5 | selection | Mod-b, Mod-z, historyRedo |
formatting shortcuts, toolbar aria-pressed, undo and redo |
shortcuts format the selection and undo restores it |
| R6 | editable | paste | clipboard HTML is parsed into schema nodes; scripts and event handlers never reach the DOM | paste parses sanitized HTML and plain text into the document |
| R7 | IME | composition events | the IME owns the DOM while composing; compositionend commits once; cancelled compositions restore the model's DOM |
composition text is committed once at compositionend and the DOM is repaired |
| R8 | toolbar | state, mousedown, click, arrows, Alt+F10, Escape | rich-text-toolbar.vue and rich-text-toolbar-button.vue: disabled when a command cannot run, keep the selection, roving focus, Alt+F10 enters, Escape returns |
toolbar buttons reflect command state, keep focus, and rove with arrows |
| R9 | focused, non-empty selection | select | rich-text-bubble-menu.vue shows a positioned toolbar and hides for a caret |
the bubble menu appears for a focused non-empty selection |
| R10 | editable=false |
input, shortcuts | aria-readonly, no mutations or events, tools disabled |
read-only editors refuse input and disable tools |
| R11 | controlled | typing / prop changes | v-model round-trips the document |
controlled documents follow v-model and external replacements |
| R12 | SSR | two requests | byte-identical static, escaped, sanitized HTML; bubble menu hidden | renders the document as static, escaped, sanitized HTML on the server |
| R13 | SSR | hydrate | no mismatch diagnostics; content node reused | hydrates the rendered document without mismatches |
| R14 | schema | define | names, textblock default, and list/listItem pairs are validated | schemas validate names, a textblock default, and list pairs |
| R15 | URLs | links and images | only safe schemes and relative URLs render | URL guards allow safe schemes and relative URLs only |
| R16 | documents | validate | structural validation against the schema (roles, lists, code blocks, marks) | documents are validated structurally against the schema |
| R17 | inline | normalize / slice | equal-mark runs merge; offsets slice across runs | inline helpers merge equal marks and slice by offsets |
| R18 | caret | insertText / toggleMark | active and stored marks apply to typed text | typing inserts text with active and stored marks |
| R19 | link | type after it | non-inclusive marks do not extend; unsafe hrefs never render | links are not inclusive: typing after a link leaves it |
| R20 | range | toggleMark | marks toggle across blocks; isMarkActive reports |
marks toggle across multi-block selections and report activity |
| R21 | textblocks | setBlockType / toggle | type changes, toggles back, code blocks strip marks and refuse marks | block types change, toggle back, and code blocks strip marks |
| R22 | caret | splitBlock | split, headings continue as paragraphs, code inserts newlines | Enter splits blocks, leaves headings as paragraphs, and inserts newlines in code |
| R23 | list item | splitBlock | split items; an empty item leaves the list | Enter in lists splits items and leaves the list from an empty item |
| R24 | caret / range | deleteBackward | characters, surrogate pairs, words, selections, joins, heading reset | Backspace deletes characters, surrogate pairs, words, selections, and joins blocks |
| R25 | start of item/quote | deleteBackward | lift out of the first list item or quote; later items merge | Backspace at the start of a list item or quote lifts it out |
| R26 | caret | deleteForward | next character or merge the next block | Delete removes the next character or merges the next block |
| R27 | blocks | wrapIn / toggleWrap | lists and quotes wrap sibling blocks, unwrap, and map the selection | wrapping in lists and quotes, toggling off, and mapping the selection |
| R28 | caret | insertInline | images and hard breaks insert with sanitized attributes | inline leaves insert at the caret with sanitized attributes |
| R29 | history | typing / format / undo / redo | typing groups into one step, formatting is separate, redo replays | history groups typing, separates formatting, and supports redo |
| R30 | transactions | create | selections clamp; invalid documents are rejected | transactions clamp selections and reject invalid documents |
| R31 | small schema | undeclared types | commands for undeclared types do not apply | commands for undeclared types are rejected at runtime |
| R32 | paste model | insertContent | edge textblocks merge, middle blocks insert, code blocks receive plain text, list items split | inserting content merges edge textblocks and keeps middle blocks |
| R33 | serializer | toHtml | escaped text, shared mark elements, editor hooks and caret fillers | serializes escaped text, nested marks, and editor hooks |
| R34 | parser | fromHtml | schema nodes survive; scripts, handlers, unsafe URLs, and unknown elements are dropped or unwrapped | parses pasted HTML into schema nodes and drops everything else |
| R35 | parser | empty input | one empty default block | parsing an empty fragment yields one empty block |
| R36 | typing | markdown prefixes | #, >, -, 1., and ``` rules build structure only at block start |
block input rules turn markdown prefixes into structure |
| R37 | typing | delimiters | **, _, `, and ~~ wrap text in marks |
mark input rules wrap delimited text and stop extending it |
| R38 | code block | typing | no rules in code; a rule undoes in one step | input rules are skipped in code blocks and undo as one step |
| R39 | keys | normalize | Mod covers Ctrl/Cmd; physical digits for Alt chords |
key names normalize modifiers and physical digits |
| R40 | keymap | defaults | marks, headings, lists, quotes, code, history, and editing keys | the default keymap binds marks, headings, lists, and history |
Notes
- The browser never mutates the document:
beforeinputis prevented and translated into commands. Composition is the exception; the IME's text is committed atcompositionendand the DOM is re-rendered from the model. - Content renders through
v-htmlofrichTextToHtml(…, { editor: true }), which escapes text, drops event attributes, and passes every attribute through the schema's render specs (links and images are URL-checked). - Collaboration, tables, and drag-and-drop of content are out of scope for v1.