Skip to content
ShineUI
Install

Rich text editor

ProseMirror in ShineUI clothes.

<sui-rich-text-editor><sui-edit-box><sui-mini-toolbar>CompositionsStable

About

Rich text on ProseMirror, in ShineUI clothes, for letters, notes, report commentary and dispute responses. classic shows the full toolbar, compact the memo-field subset and bubble only a selection toolbar. Markdown shortcuts convert as you type, merge fields insert as chips, and html is the form value.

Built from

Usage

<sui-rich-text-editor variant="classic" label="Engagement letter" min-height="280" help="Try markdown shortcuts: type “## ”, “- ”, “> ” or “```” at a line start. ⌘B, ⌘I and ⌘U work too."> <span slot="status" style="display:inline-flex;align-items:center;gap:5px;font-weight:650;color:var(--ink-muted)"><i class="fa-solid fa-circle-check" style="font-size:10px"></i>Draft saved just now</span> </sui-rich-text-editor> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-rich-text-editor"); el.mergeFields = [ { token: "customer.name", label: "Customer name" }, { token: "invoice.number", label: "Invoice #" }, { token: "invoice.balance", label: "Balance due" }, { token: "invoice.due_date", label: "Due date" }, { token: "company.advisor", label: "Advisor" }, ]; /* SuiMergeField[] */ el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiRichTextEditor } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const mergeFields = [ { token: "customer.name", label: "Customer name" }, { token: "invoice.number", label: "Invoice #" }, { token: "invoice.balance", label: "Balance due" }, { token: "invoice.due_date", label: "Due date" }, { token: "company.advisor", label: "Advisor" }, ]; /* SuiMergeField[] */ return ( <SuiRichTextEditor variant="classic" label="Engagement letter" minHeight="280" help="Try markdown shortcuts: type “## ”, “- ”, “> ” or “```” at a line start. ⌘B, ⌘I and ⌘U work too." mergeFields={mergeFields} onSuiChange={(e) => console.log(e.detail)}> <span slot="status" style="display:inline-flex;align-items:center;gap:5px;font-weight:650;color:var(--ink-muted)"><i class="fa-solid fa-circle-check" style="font-size:10px"></i>Draft saved just now</span> </SuiRichTextEditor> ); }
<script setup> import { SuiRichTextEditor } from "@shineui/vue"; import "@shineui/core/shineui.css"; const mergeFields = [ { token: "customer.name", label: "Customer name" }, { token: "invoice.number", label: "Invoice #" }, { token: "invoice.balance", label: "Balance due" }, { token: "invoice.due_date", label: "Due date" }, { token: "company.advisor", label: "Advisor" }, ]; // SuiMergeField[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiRichTextEditor variant="classic" label="Engagement letter" min-height="280" help="Try markdown shortcuts: type “## ”, “- ”, “> ” or “```” at a line start. ⌘B, ⌘I and ⌘U work too." :merge-fields="mergeFields" @sui-change="onChange"><span slot="status" style="display:inline-flex;align-items:center;gap:5px;font-weight:650;color:var(--ink-muted)"><i class="fa-solid fa-circle-check" style="font-size:10px"></i>Draft saved just now</span></SuiRichTextEditor> </template>
import { Component } from "@angular/core"; import { SuiRichTextEditor } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiRichTextEditor], template: `<sui-rich-text-editor variant="classic" label="Engagement letter" min-height="280" help="Try markdown shortcuts: type “## ”, “- ”, “> ” or “```” at a line start. ⌘B, ⌘I and ⌘U work too." [mergeFields]="mergeFields" (suiChange)="onChange($event)"><span slot="status" style="display:inline-flex;align-items:center;gap:5px;font-weight:650;color:var(--ink-muted)"><i class="fa-solid fa-circle-check" style="font-size:10px"></i>Draft saved just now</span></sui-rich-text-editor>`, }) export class Example { mergeFields = [ { token: "customer.name", label: "Customer name" }, { token: "invoice.number", label: "Invoice #" }, { token: "invoice.balance", label: "Balance due" }, { token: "invoice.due_date", label: "Due date" }, { token: "company.advisor", label: "Advisor" }, ]; // SuiMergeField[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

classic shows the full toolbar with the block picker, merge fields and history, and the ProseMirror engine loads once for every editor on the page. Markdown shortcuts convert live: type "## ", "- ", "> " or "```" at a line start; there is no raw markdown mode.

Draft saved just now
<sui-rich-text-editor id="rte-basic" variant="classic" label="Engagement letter" min-height="280" help="Try markdown shortcuts: type “## ”, “- ”, “> ” or “```” at a line start. ⌘B, ⌘I and ⌘U work too."> <span slot="status" style="display:inline-flex;align-items:center;gap:5px;font-weight:650;color:var(--ink-muted)"><i class="fa-solid fa-circle-check" style="font-size:10px"></i>Draft saved just now</span> </sui-rich-text-editor>
// setup(root): properties and events setup(root) { const ed = root.querySelector('#rte-basic'); ed.mergeFields = mergeFields(); ed.html = letter; }

Compact

compact keeps bold, italic, underline, lists and link, for memo fields inside forms.

<sui-rich-text-editor variant="compact" label="Internal memo" placeholder="Add a note for the file" html="<p>Vendor confirmed the <strong>paper stock substitution</strong> on Aug 6.</p>"></sui-rich-text-editor>

Bubble

bubble shows no fixed bar. Select text and a sui-mini-toolbar appears above the selection with the four marks.

<sui-rich-text-editor variant="bubble" label="Report commentary" min-height="112" html="<p>Gross margin held at <strong>47.5%</strong> despite freight pressure. Select any of this text to format it with the floating toolbar.</p>"></sui-rich-text-editor>

Invalid

error paints the border in the danger tone, replaces the help line and sets aria-invalid on the document.

<sui-rich-text-editor variant="compact" label="Dispute response" error="A response is required before escalating." min-height="64"></sui-rich-text-editor>

Read-only and disabled

readonly locks the document on sunken paper with the lock chip and drops the toolbar; the document still posts. disabled locks it the same way and posts nothing, as a disabled form or fieldset does. invalid paints the danger border alone.

<div style="display:grid;gap:14px"> <sui-rich-text-editor readonly variant="bubble" label="July commentary" min-height="80" html="<p>July commentary was <u>locked at close</u> on Aug 4 by Dana Whitfield. Contact your advisor to reopen.</p>"></sui-rich-text-editor> <sui-rich-text-editor disabled variant="bubble" label="June commentary" min-height="80" html="<p>June commentary is archived with the closed period.</p>"></sui-rich-text-editor> </div>

In a form

The element is form associated and the form value is the HTML under name. required holds the submit while the document has no text. Reset returns the document to the HTML it started with.

Submit to see the posted value.
<form id="rte-form" style="display:flex;flex-direction:column;gap:12px;max-width:560px"> <sui-rich-text-editor variant="compact" name="memo" label="Internal memo" required html="<p>Vendor confirmed the <strong>paper stock substitution</strong> on Aug 6.</p>"></sui-rich-text-editor> <div style="display:flex;gap:8px"> <sui-button type="submit" variant="brand" size="sm" label="Save memo"></sui-button> <sui-button type="reset" variant="outline" size="sm" label="Reset"></sui-button> </div> <output id="rte-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see the posted value.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#rte-form'); const out = root.querySelector('#rte-out'); form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v}`).join(' '); }); form.addEventListener('reset', () => { out.textContent = 'Reset to the starting HTML.'; }); }

Events

suiChange fires on every document change with the HTML, the plain text and the word and character counts. suiSelection fires after every transaction with the snapshot, which is how a host mirrors the active marks.

Type to see the counters.
<div style="display:flex;flex-direction:column;gap:10px;max-width:560px"> <sui-rich-text-editor id="rte-events" variant="compact" label="Advisor note" html="<p>Municipal payers run slower in Q3, watch <strong>Clark County</strong>.</p>"></sui-rich-text-editor> <span id="rte-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Type to see the counters.</span> </div>
// setup(root): properties and events setup(root) { const ed = root.querySelector('#rte-events'); const log = root.querySelector('#rte-log'); ed.addEventListener('suiChange', (e) => { log.textContent = `suiChange: ${e.detail.words} words, ${e.detail.chars} characters.`; }); ed.addEventListener('suiSelection', (e) => { const marks = Object.keys(e.detail.marks || {}).filter((k) => e.detail.marks[k]); if (marks.length) log.textContent = `suiSelection: ${marks.join(', ')} at the cursor.`; }); }

Basicsui-edit-box

Static prose until clicked, then a live ProseMirror mount in the same box; Done serializes back to HTML and emits suiSave, Cancel or Escape discards. The view box is a button named "Edit" plus the label.

<sui-edit-box label="Executive summary" html="<p><strong>Q3 outlook.</strong> Revenue grew 10.7% year over year; margin held at <u>47.5%</u> despite freight pressure.</p>"></sui-edit-box>

Floating toolbarsui-edit-box

toolbar="floating" keeps the exact view-mode box metrics and anchors a sui-mini-toolbar at the caret, so entering edit mode shifts nothing.

<div style="padding-top:30px"> <sui-edit-box label="Advisor note" toolbar="floating" html="<p>Municipal payers run slower in Q3, watch <strong>Clark County</strong> and New Albany Parks.</p>"></sui-edit-box> </div>

Emptysui-edit-box

placeholder shows in view mode while there is no content.

<sui-edit-box label="Advisor note" placeholder="Click to add an advisor note"></sui-edit-box>

Disabledsui-edit-box

disabled keeps the text readable, opens no editor and posts nothing; a disabled form or fieldset does the same.

<div style="max-width:560px"><sui-edit-box label="Locked commentary" name="locked" html="<p>Q3 closed strong. Collections improved after the reminder cadence was moved to seven days.</p>" disabled></sui-edit-box></div>

Read-only, required and messagessui-edit-box

readonly shows the text as a read-only text box: it keeps its tab stop, opens no editor and still posts. required holds the form while the saved HTML has no text. help and error draw the field lines under the box, and error paints its edge and fails the form.

<div style="display:grid;gap:18px;max-width:560px"> <sui-edit-box label="Approved summary" name="approved" readonly html="<p>Signed off by the audit committee on Aug 4.</p>"></sui-edit-box> <sui-edit-box label="Risk note" name="risk" required help="One or two sentences." placeholder="Click to write the risk note"></sui-edit-box> <sui-edit-box label="Variance note" name="variance" html="<p>Costs rose.</p>" error="Say which costs rose and by how much."></sui-edit-box> </div>

In a formsui-edit-box

The element is form associated and the form value is the saved HTML under name. Edits count only after Done; Reset returns the box to the HTML it started with.

Submit to see the posted value.
<form id="eb-form" style="display:flex;flex-direction:column;gap:12px;max-width:560px"> <sui-edit-box name="summary" label="Executive summary" html="<p>Revenue grew 10.7% year over year.</p>"></sui-edit-box> <div style="display:flex;gap:8px"> <sui-button type="submit" variant="brand" size="sm" label="Save summary"></sui-button> <sui-button type="reset" variant="outline" size="sm" label="Reset"></sui-button> </div> <output id="eb-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see the posted value.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#eb-form'); const out = root.querySelector('#eb-out'); form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${v}`).join(' '); }); form.addEventListener('reset', () => { out.textContent = 'Reset to the starting HTML.'; }); }

Eventssui-edit-box

suiSave carries the serialized HTML when Done commits; suiCancel fires with no detail when Cancel or Escape discards. edit(), save() and cancel() drive the same three moves from script.

Idle.
<div style="display:flex;flex-direction:column;gap:10px;max-width:560px"> <sui-edit-box id="eb-events" label="Advisor note" html="<p>Watch <strong>Clark County</strong>.</p>"></sui-edit-box> <div style="display:flex;gap:8px;align-items:center"> <sui-button id="eb-edit" variant="outline" size="sm" label="Edit from script"></sui-button> <span id="eb-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div> </div>
// setup(root): properties and events setup(root) { const box = root.querySelector('#eb-events'); const log = root.querySelector('#eb-log'); box.addEventListener('suiSave', (e) => { log.textContent = `suiSave: ${e.detail.length} characters of HTML.`; }); box.addEventListener('suiCancel', () => { log.textContent = 'suiCancel: the edit was discarded.'; }); root.querySelector('#eb-edit').addEventListener('click', () => { box.edit(); }); }

Basicsui-mini-toolbar

A white capsule positioned at x and y within the nearest relative parent, outside the text flow, shown while open is set. It carries role="toolbar"; the parent owns clamping to its bounds.

Municipal payers run slower in Q3, watch Clark County and New Albany Parks.
<div style="position:relative;padding-top:52px"> <div style="border:1px solid var(--border-strong);border-radius:var(--radius-md);padding:9px 12px;font-size:13.5px;line-height:1.6;background:var(--surface)">Municipal payers run slower in Q3, watch <strong>Clark County</strong> and New Albany Parks.</div> <sui-mini-toolbar open x="12" y="4" aria-label="Formatting"> <button type="button" class="sui-tbtn active" aria-label="Bold" aria-pressed="true" style="height:28px;min-width:28px;border:none;border-radius:6px;background:var(--brand-soft);color:var(--brand);cursor:pointer"><i class="fa-solid fa-bold" style="font-size:12px"></i></button> <button type="button" class="sui-tbtn" aria-label="Italic" aria-pressed="false" style="height:28px;min-width:28px;border:none;border-radius:6px;background:transparent;color:var(--ink-muted);cursor:pointer"><i class="fa-solid fa-italic" style="font-size:12px"></i></button> <button type="button" class="sui-tbtn" aria-label="Underline" aria-pressed="false" style="height:28px;min-width:28px;border:none;border-radius:6px;background:transparent;color:var(--ink-muted);cursor:pointer"><i class="fa-solid fa-underline" style="font-size:12px"></i></button> <span role="separator" aria-orientation="vertical" style="width:1px;height:18px;background:var(--hairline);margin:0 3px"></span> <sui-button variant="outline" size="sm" label="Cancel"></sui-button> <span style="width:4px"></span> <sui-button variant="brand" size="sm" label="Done"></sui-button> </sui-mini-toolbar> </div>

Hidden until neededsui-mini-toolbar

The capsule is a contextual surface and starts closed: it renders nothing and is hidden from the accessibility tree until the host sets open, at a selection say; here the button sets it.

The formatting capsule opens over this line.
<div style="position:relative;padding-top:52px"> <div style="border:1px dashed var(--border-strong);border-radius:var(--radius-md);padding:9px 12px;font-size:13.5px;line-height:1.6">The formatting capsule opens over this line.</div> <sui-mini-toolbar x="12" y="4" aria-label="Formatting"><sui-button variant="ghost" size="sm" label="Bold"></sui-button></sui-mini-toolbar> <sui-button id="mt-toggle" variant="outline" size="sm" label="Show formatting" style="margin-top:10px"></sui-button> </div>
// setup(root): properties and events setup(root) { const bar = root.querySelector('sui-mini-toolbar'); const toggle = root.querySelector('#mt-toggle'); toggle.addEventListener('click', () => { bar.open = !bar.open; toggle.label = bar.open ? 'Hide formatting' : 'Show formatting'; }); }

Data contract

Documents serialize to ProseMirror JSON. Markdown shortcuts apply as you type. The full page with modes, blocks and rules is the Editor link in the top bar.

How the editor behaves

The rules the chrome follows, so a document written in one place looks like a document written in another.

  • Chrome is quiet. 28px ghost toolbar buttons. Active is brand-soft plus navy, never orange. The one orange element is the merge field.
  • Merge fields move as one. Dashed accent-soft chips. You cannot type inside one, it selects as a single unit, and it resolves when you send. Never a raw token in prose.
  • Markdown is input, not a mode. Type ##, -, > or ``` and the line converts as you type. There is no second raw-markdown pane, so formatting never splits in two.
  • Sentence case throughout. Toolbar tooltips carry their shortcut, menus are verb first, and the status bar reports words, characters and save state in plain words.
  • Locked documents look locked. A read-only document renders on sunken paper with the lock chip, the same treatment a closed period gets.
  • Money columns stay aligned. Editor tables inherit the table system: sunken header, money columns right-aligned on fixed-width digits.

The engine, without the chrome

utils/editor-core wraps the ProseMirror packages behind one lazy loader, so the engine downloads on first use and every editor on the page shares one module instance. The schema adds underline and strikethrough marks and the merge_field atom.

To build your own toolbar, call createEditor and drive it. It returns the commands, the serializers and a teardown, and it holds no UI of its own.

import { createEditor } from "@shineui/core"; const editor = await createEditor(mountEl, { html, readOnly: false, onState: (snapshot) => render(snapshot), }); editor.cmd.bold(); // bold, italic, underline, strike, code editor.cmd.block("h2"); // p, h1, h2, h3, code editor.cmd.bulletList(); // bulletList, orderedList, outdent, quote, unquote editor.cmd.link(href); // link, unlink editor.cmd.mergeField("customer.name", "Customer name"); editor.toHTML(); editor.getText(); editor.snapshot(); editor.destroy();

Keyboard

KeyAction
Ctrl+B Ctrl+I Ctrl+U sui-rich-text-editorBold, italic and underline in the document (Cmd on a Mac).
Ctrl+Z Ctrl+Shift+Z sui-rich-text-editorUndo and redo (Cmd on a Mac; Ctrl+Y also redoes).
ArrowLeft ArrowRight sui-rich-text-editorIn the toolbar, move between the buttons in reading order. The toolbar is one tab stop.
ArrowDown ArrowUp sui-rich-text-editorIn the block type or merge field menu, move between the options, wrapping.
Home End sui-rich-text-editorIn the toolbar, jump to the first or last button; in an open menu, to the first or last option.
Enter sui-rich-text-editorIn the link box, applies the address.
Escape sui-rich-text-editorCloses an open menu or link box and returns focus to the document.
Enter Space sui-edit-boxOn the view box, enters edit mode and focuses the document.
Ctrl+B Ctrl+I Ctrl+U sui-edit-boxBold, italic and underline while editing (Cmd on a Mac).
Ctrl+Z Ctrl+Shift+Z sui-edit-boxUndo and redo while editing (Cmd on a Mac; Ctrl+Y also redoes).
Escape sui-edit-boxWhile editing, discards the edit and returns focus to the view box.
ArrowRight ArrowLeft sui-mini-toolbarMove between the buttons and links inside the capsule, which is one tab stop.
Home End sui-mini-toolbarJump to the first or last button in the capsule.

API

<sui-rich-text-editor>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseDisables the editor: the document locks as with readOnly, and the element posts nothing.
errorerrorstringstring | undefinedError line under the editor; also paints the border in the danger tone.
helphelpstringstring | undefinedHelp line under the editor (hidden while error is set).
htmlhtmlstring''Document as HTML (parsed through the ShineUI schema). Initial and controlled content: the element writes the serialized document back on every change and re-parses when set from outside. The string is read through sanitizeEditorHtml in an inert document before the schema sees it, so a script, a handler attribute or a javascript: link in a stored value never runs; a link the schema will not keep is shown as plain text. A picture is drawn and written back with referrerpolicy="no-referrer". Markdown shortcuts (##, -, >, "```") convert live as you type; there is no raw-markdown mode.
invalidinvalidbooleanfalseDanger border without a message, for a form that draws its summary elsewhere.
labellabelstringstring | undefinedField label above the editor. Without a visible label, a host aria-label or aria-labelledby names the editor the way it names every text field.
mergeFieldsproperty onlySuiMergeField[][]Merge-field tokens offered by the "Merge field" menu (property, not attribute). Each inserts as a dashed accent chip that resolves at send time; never type raw {{tokens}}.
minHeightmin-heightnumbernumber | undefinedMinimum height of the writing area in px (default 170; compact 84).
namenamestringstring | undefinedForm control name; the form value is the HTML. Reflected: the form entry is built from the content attribute, and all three framework bindings set properties.
placeholderplaceholderstringstring | undefinedShown while the document is empty.
readOnlyreadonlybooleanfalseLocks the document: sunken paper, lock chip, no toolbar. A disabled form does the same.
requiredrequiredbooleanfalseThe document must hold some text for the surrounding form to be valid (valueMissing while it is empty; a picture alone counts as empty). A read-only or disabled editor never holds the form.
variantvariant"bubble" | "classic" | "compact"'classic'classic = full toolbar (block picker, marks, lists, quote, link, merge fields, history); compact = bold, italic, underline, lists and link for memo fields; bubble = no fixed bar, only a sui-mini-toolbar at the selection.

Events

EventDetailReactVueDescription
suiChangeSuiRichTextChangeDetailonSuiChange@sui-changeFires on every document change with the serialized HTML, plain text and counters.
suiSelectionSuiEditorSnapshotonSuiSelection@sui-selectionFires after every transaction (selection or document) with the fresh snapshot. A state report, not a value: it fires on every caret move. Does not bubble.

Methods

MethodSignatureDescription
exec()exec(cmd: SuiEditorCommandName, arg?: string, arg2?: string) => Promise<void>Runs an engine command: exec('bold'), exec('block', 'h2'), exec('link', href), exec('mergeField', token, label).
getSnapshot()getSnapshot() => Promise<SuiEditorSnapshot | undefined>Current snapshot (marks, block, counters, history), or undefined before the engine is ready.
setFocus()setFocus() => Promise<void>Moves keyboard focus into the document.

Slots

SlotDescription
statusRight side of the status bar (a save-status line pairs well).

Types

SuiEditorBlockName typeBlock types the block command and the block picker understand.
No members.
SuiEditorCommandName type
No members.
SuiEditorCommands interfaceThe command map, identical to pm-core.js api.cmd. Every command refocuses the view.
bold() => void
italic() => void
underline() => void
strike() => void
code() => void
block(name: SuiEditorBlockName | string) => void
bulletList() => void
orderedList() => void
outdent() => void
quote() => void
unquote() => void
undo() => void
redo() => void
link(href: string) => voidWraps the selection in a link. An address that is not http, https, mailto, tel or sms, or a relative path, applies no link at all.
unlink() => void
mergeField(token: string, label: string) => void
SuiEditorSnapshot interfaceWhich marks / block context the selection sits in, plus document counters.
marks{ bold: boolean; italic: boolean; underline: boolean; strike: boolean; code: boolean; link: boolean; }
blockstringp, h1..h3, code, or another node type name.
list'bullet' | 'ordered' | null
quoteboolean
wordsnumber
charsnumber
canUndoboolean
canRedoboolean
hasSelectionboolean
emptybooleanTrue when the document has no visible text.
SuiMergeField interfaceA merge-field token the "Merge field" menu offers. Tokens resolve at send time.
tokenstringMachine token, e.g. customer.name (rendered as {{customer.name}} in the menu).
labelstringHuman label shown inside the chip.
SuiRichTextChangeDetail interfaceDetail of suiChange: the serialized document and its counters.
htmlstring
textstring
wordsnumber
charsnumber

<sui-edit-box>

Properties

PropertyAttributeTypeDefaultDescription
disableddisabledbooleanfalseDisables the box: the text stays readable, nothing opens the editor, and the element posts nothing. A disabled form does the same.
errorerrorstringstring | undefinedError message under the box; paints its edge in the danger tone, sets aria-invalid and fails the form (customError).
helphelpstringstring | undefinedHelp text under the box. Replaced by error when set.
htmlhtmlstring''Current content as HTML (ShineUI schema). Mutable; updated when an edit is saved. View mode and the edit path both read it through sanitizeEditorHtml: tags and attributes the editor cannot produce are dropped, a script or a handler never reaches the page, and a link whose address is not http, https, mailto, tel or sms, or a relative path, is shown as plain text. A picture is shown with referrerpolicy="no-referrer". View mode writes the sanitised markup through the library's Trusted Types policy, shineui. The form value is the string as set, so a value that came from your own store posts back unchanged until an edit is saved.
invalidinvalidbooleanfalseDanger edge without a message, for a form that draws its summary elsewhere.
labellabelstringstring | undefinedLabel above the box; also names the edit control ("Edit {label}").
minHeightmin-heightnumber56Minimum box height in px in both modes.
namenamestringstring | undefinedForm control name; the form value is the saved HTML. Reflected: the form entry is built from the content attribute, and all three framework bindings set properties.
placeholderplaceholderstringstring | undefinedShown in view mode while there is no content.
readOnlyreadonlybooleanfalseDisplay only: the text can be read, selected and copied, nothing opens the editor, and the value still posts. The box keeps its tab stop as a read-only text box. Setting it while an edit is open discards the edit.
requiredrequiredbooleanfalseThe box must hold some text for the surrounding form to be valid (valueMissing while the saved HTML has none; a picture alone counts as empty). A read-only or disabled box never holds the form.
toolbartoolbar"attached" | "floating"'attached'attached = a small bar above the box; floating = the box keeps its exact view-mode metrics and a sui-mini-toolbar sits at the caret, so entering edit mode shifts nothing.

Events

EventDetailReactVueDescription
suiCancelvoidonSuiCancel@sui-cancelFires when an edit is discarded (Cancel or Escape).
suiSavestringonSuiSave@sui-saveFires when Done commits an edit; detail is the serialized HTML.

Methods

MethodSignatureDescription
cancel()cancel() => Promise<void>Discards the current edit and returns to view mode.
edit()edit() => Promise<void>Enters edit mode (mounts the engine and focuses it).
exec()exec(cmd: SuiEditorCommandName, arg?: string, arg2?: string) => Promise<void>Runs an engine command while editing: exec('bold'), exec('block', 'h2').
save()save() => Promise<void>Serializes the live document, leaves edit mode and emits suiSave.
setFocus()setFocus() => Promise<void>Moves keyboard focus to the box without entering edit mode; while editing it focuses the document. sui-form calls this to reach the first invalid control.

Types

SuiEditorBlockName typeBlock types the block command and the block picker understand.
No members.
SuiEditorCommandName type
No members.
SuiEditorCommands interfaceThe command map, identical to pm-core.js api.cmd. Every command refocuses the view.
bold() => void
italic() => void
underline() => void
strike() => void
code() => void
block(name: SuiEditorBlockName | string) => void
bulletList() => void
orderedList() => void
outdent() => void
quote() => void
unquote() => void
undo() => void
redo() => void
link(href: string) => voidWraps the selection in a link. An address that is not http, https, mailto, tel or sms, or a relative path, applies no link at all.
unlink() => void
mergeField(token: string, label: string) => void

<sui-mini-toolbar>

Properties

PropertyAttributeTypeDefaultDescription
openopenbooleanfalseShows the capsule. It is a contextual surface: the host opens it where it is wanted, at a text selection or the caret, and closes it; the toolbar never changes it itself, so it fires no open-state events. Closed, it fades out over the fast duration, then renders nothing visible and is hidden from the accessibility tree. The capsule is a role="toolbar" with one tab stop. Reflected.
xxnumber0Left offset in px within the nearest relative parent.
yynumber0Top offset in px within the nearest relative parent.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves keyboard focus to the capsule's one tab stop while it is open.

Slots

SlotDescription
(default)Toolbar buttons (sui-rich-text-editor buttons, sui-button, compact actions).

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

<sui-rich-text-editor>

Color27

--accentthe one chart highlight: the flagged series, down candles, the peak cell--accent-deeperror borders, destructive emphasis--accent-softorange 18% on white, warning fills, highlights--accent-soft-ink--bordercard + table outer borders--brand-softnavy 10% on white, quiet chips, selected rows--brand-soft-ink--control-borderthe resting edge of every field and control: 3:1 on its grounds--danger--disabled-bg--disabled-ink--focus-halo--focus-ring--hairlinerow separators--ink--ink-faintmetadata beside body ink: 4.8:1 on the page grounds (5.4:1 in dark)--ink-mutedsecondary text: 7:1 on the page grounds and 1.5 times faint--ink-muted-on-washthe muted ink a selected, current or pressed row paints: 4.5:1 on that wash--ink-placeholdera field's placeholder, not a text tier: 4.5:1 on the page grounds--link--sui-code-dark-bgthe dark code pane: the chrome, or a deep tone when the chrome is light--sui-code-dark-ink--surfacecards, tables, inputs--surface-hovera hovered row or quiet control on any ground: the ink, translucent--surface-raisedmenus, popovers, toasts: the card in light, a step above it in dark--surface-selecteda selected row or option: the primary's hue, always heavier than a hover--surface-sunkentable headers, wells

Typography2

Ag--font-monocode, IDs, terminal onlyAg--font-sans

Spacing and size5

42--control-lgform-page inputs34--control-mdDEFAULT toolbar/filter controls12--control-pad-x28--control-sm700--z-dropdown

Radius1

--radius-mdALL controls: inputs, buttons, selects

Elevation2

--focus-shadow--shadow-popdropdowns, popovers, toasts

Motion3

--duration-fasthovers, presses, every exit--duration-overlayevery entrance: palette, popover, dialog, drawer, toast, menu, select list--ease-out

<sui-edit-box>

<sui-mini-toolbar>

Every name above is listed with its value and its role in Foundations.

See also