Skip to content

Rich Text Editor

Form InputsSince 0.4.0
import { RichTextEditor } from '@absoluteui/components/rich-text-editor';
import '@absoluteui/components/rich-text-editor/style.css';

Using Angular? See Rich Text Editor for Angular.

Formatted multi-line text entry. RichTextEditor renders a contenteditable textbox whose content is owned by an internal document model. Every edit arrives as an Input Events beforeinput (or a key command), is cancelled, applied to the model, and the affected blocks are re-rendered; the browser’s editing engine never changes the DOM, except during IME composition, which is reconciled from the DOM at compositionend. document.execCommand is not used.

Formatting set: bold, italic, underline, strikethrough, inline code, links, normal text, headings 1-3, bulleted and numbered lists (nested to depth 4), block quotes, code blocks, and alignment (start, center, end, justify). The editor also provides undo/redo, sanitized HTML and Markdown input and output, Markdown typing shortcuts, a plain-text mode, and the same form-control contract as Textarea: label, required indicator, help text, localized validation, and an optional character counter.

  • Formatted content that users write themselves: release notes, descriptions, comments, knowledge-base text.
  • Values stored as sanitized HTML or Markdown that must round-trip without unsafe markup.
  • Multi-paragraph plain text where paragraphs matter and a consistent editing surface with the rich variant is wanted (mode ‘plain’).
  • Unformatted text with no paragraph structure: use Textarea (native, lighter).
  • Single-line values: use Text Input.
  • Documents that need tables, images, colors, fonts, or embedded media: not supported.
  • Code editing with syntax highlighting: code blocks are plain monospace text.

Required HTML field with a character limit

Section titled “Required HTML field with a character limit”
import { RichTextEditor } from '@absoluteui/components/rich-text-editor';
const host = document.createElement('div');
document.body.append(host);
const notes = RichTextEditor.create(host, {
label: 'Release notes',
name: 'notes',
required: true,
maxLength: 2000,
showCounter: true,
value: '<p>Fixed <strong>two</strong> bugs.</p>',
});
host.addEventListener('aui-change', () => {
host.dataset.valid = String(notes.validity.valid);
});
import { RichTextEditor, type RichTextToolbarItem } from '@absoluteui/components/rich-text-editor';
const toolbar: readonly RichTextToolbarItem[] = ['heading2', 'heading3', '|', 'bold', 'italic', 'code', 'link', '|', 'bulletList', 'orderedList', '|', 'undo', 'redo'];
const host = document.createElement('div');
document.body.append(host);
const editor = RichTextEditor.create(host, {
label: 'Description',
valueFormat: 'markdown',
headings: [2, 3],
toolbar,
linkSchemes: ['https', 'mailto'],
value: '## Summary\n\nSee [the docs](https://example.com/docs).',
});
const html: string = editor.getHTML();
const markdown: string = editor.value;
host.dataset.length = String(html.length + markdown.length);

Plain-text mode (paragraphs only, no toolbar)

Section titled “Plain-text mode (paragraphs only, no toolbar)”
import { RichTextEditor } from '@absoluteui/components/rich-text-editor';
const host = document.createElement('div');
document.body.append(host);
const summary = RichTextEditor.create(host, { label: 'Summary', mode: 'plain', rows: 3, maxRows: 8, placeholder: 'One or two paragraphs' });
summary.setText('First paragraph\nSecond paragraph');

External command buttons driven by formatState

Section titled “External command buttons driven by formatState”
import { RichTextEditor } from '@absoluteui/components/rich-text-editor';
import { Button } from '@absoluteui/components/button';
const host = document.createElement('div');
const bold = document.createElement('button');
bold.type = 'button';
bold.textContent = 'Bold';
Button.enhance(bold, { size: 'sm' });
document.body.append(bold, host);
const editor = RichTextEditor.create(host, { label: 'Comment', toolbar: ['undo', 'redo'] });
// Keep the editor's selection when the button is pressed with the mouse.
bold.addEventListener('mousedown', (e) => {
e.preventDefault();
});
bold.addEventListener('click', () => {
editor.command('bold');
bold.setAttribute('aria-pressed', String(editor.formatState.marks.includes('bold')));
});

Convert Markdown to sanitized HTML without an editor

Section titled “Convert Markdown to sanitized HTML without an editor”
import { richTextFromMarkdown, richTextToHtml } from '@absoluteui/components/rich-text-editor';
const httpsOnly = (url: string): string | null => (url.startsWith('https://') ? url : null);
const doc = richTextFromMarkdown('# Notes\n\n- **one**\n- [two](https://example.com)', httpsOnly);
const html: string = richTextToHtml(doc);
document.body.dataset.preview = html;

Disable Markdown shortcuts and shorten undo history globally

Section titled “Disable Markdown shortcuts and shorten undo history globally”
import { configureAbsoluteUI } from '@absoluteui/core';
configureAbsoluteUI({ components: { richTextEditor: { markdownShortcuts: false, historyLimit: 50 } } });
import { RichTextEditor } from '@absoluteui/components/rich-text-editor';
import '@absoluteui/components/rich-text-editor/style.css';
const host = document.querySelector<HTMLElement>('#release-notes');
if (host) {
const notes = RichTextEditor.create(host, { label: 'Release notes', name: 'notes', required: true, maxLength: 5000, showCounter: true, maxRows: 20 });
host.addEventListener('aui-change', () => {
if (!notes.validity.valid) notes.reportValidity();
});
}
import { RichTextEditor } from '@absoluteui/components/rich-text-editor';
const host = document.createElement('div');
document.body.append(host);
const editor = RichTextEditor.create(host, { label: 'Description', valueFormat: 'markdown', headings: [2, 3], maxLength: 2000, showCounter: true });
editor.setMarkdown('## Summary\n\nSee [the docs](https://example.com/docs).');
<div class="aui-field aui-rte" data-aui-control> (consumer host)
<label class="aui-field__label">Label<span class="aui-field__required" aria-hidden="true">*</span></label>
<div class="aui-rte__toolbar" role="toolbar" aria-label="Formatting" aria-controls=content> (hidden in plain mode)
<button class="aui-rte__tool" data-command="bold" aria-label="Bold" aria-pressed tabindex="0|-1">
<div class="aui-rte__separator" role="separator" aria-orientation="vertical">
<div class="aui-rte__link-panel" role="group" aria-label="Link" hidden>
<label class="aui-rte__link-label" for=address>Link address</label>
<div class="aui-rte__link-row">
<input type="url" class="aui-rte__link-input">
<button class="aui-rte__link-button aui-rte__link-apply|remove|cancel">
<div class="aui-rte__link-error aui-validation-message" hidden>
<div class="aui-rte__surface"> (border, scrolling, focus indicator)
<div class="aui-rte__content" role="textbox" aria-multiline="true" aria-labelledby=label contenteditable>
<p|h1-h3|blockquote|pre class="aui-rte__block" data-aui-block="0">...
<ul|ol class="aui-rte__list"><li class="aui-rte__block">...
<div class="aui-field__description"> (optional)
<div class="aui-field__counter"> (showCounter)
<div class="aui-field__error aui-validation-message"> (when shown)
<input type="hidden" name> (value for native submission)
</div>
RichTextEditor.create(host: HTMLElement, options: RichTextEditorOptions): RichTextEditor

Global configuration key: richTextEditor (see Configuration).

Option Type Default Description
label string Visible label and accessible name of the textbox (aria-labelledby). Required.
hideLabel boolean false Visually hide the label; it remains the accessible name.
value string "" Initial value in valueFormat (sanitized when HTML); also the value restored by reset(). Passing it to update() replaces the content.
valueFormat 'html' | 'markdown' | 'text' "html" Format of value, getFormValue(), and event details. Default ‘html’ in rich mode and ‘text’ in plain mode.
mode 'rich' | 'plain' "rich" ‘plain’: no formatting and no toolbar; paragraphs and line breaks only. Switching to ‘plain’ converts the content to plain text.
name string Form field name; a hidden input carries the value for native form submission.
placeholder string Shown while the editor is empty (also aria-placeholder); never replaces the label.
description string Help text referenced by aria-describedby.
required boolean false Empty (or whitespace-only) text is invalid; shows the required indicator and sets aria-required.
disabled boolean false Not editable and not focusable; toolbar tools are aria-disabled.
readOnly boolean false Focusable and submitted but not editable (aria-readonly).
minLength number Minimum length of the plain text in code points (integer >= 0, <= maxLength).
maxLength number Maximum length of the plain text in code points (validated, not truncated).
showCounter boolean false Show remaining/over characters; requires maxLength.
toolbar readonly RichTextToolbarItem[] ["paragraph","heading1","heading2","heading3","|","bold","italic","underline","strike","code","link","|","bulletList","orderedList","quote","codeBlock","|","alignStart","alignCenter","alignEnd","alignJustify","|","undo","redo","clearFormatting"] Toolbar items in order; ‘|’ is a separator. Heading items for levels not in headings are omitted. Hotkeys and Markdown shortcuts work regardless of the items shown.
headings readonly (1 | 2 | 3)[] [1,2,3] Heading levels offered and accepted from HTML/Markdown (non-empty); other levels map to the nearest allowed lower level.
linkSchemes readonly string[] ["http","https","mailto","tel"] URL schemes allowed in links besides relative URLs (lowercase, without ‘:’). javascript, data, and vbscript are never allowed and throw.
rows number 6 Minimum visible height in text rows, integer 1..100.
maxRows number Maximum height in rows (integer >= rows, <= 100); the content scrolls beyond it.
spellcheck boolean true Native spellcheck on the content element.
validate (value: string) => string | null Consumer validation of the value in valueFormat; return a message to mark it invalid. Runs after the built-in constraints pass.
icons Partial<{ error: IconRef }> Icon slot overrides.
validationTiming 'blur' | 'input' | 'submit' "blur" When validation messages first appear; afterwards they update live. Globally configurable.
markdownShortcuts boolean true Markdown typing shortcuts: ’# ’, ’- ’, ’1. ’, ’> ’, ‘```’ + Enter, bold, italic, code, strike (rich mode only). Globally configurable.
historyLimit number 100 Undo depth, integer 0..1000. Typing (or deleting) within 1 s is one undo step. Globally configurable.
locale string "en-US" Toolbar, link panel, announcement, and validation message locale; word and grapheme boundaries for deletion. Globally configurable. Inherited from common configuration.
direction 'ltr' | 'rtl' "ltr" ‘rtl’ sets dir=“rtl” on the content element (‘ltr’ leaves dir=“auto”) and maps formatJustifyLeft/Right to end/start. Globally configurable. Inherited from common configuration.
Property Type Description
host HTMLElement Consumer host; its children are owned by the component.
content HTMLDivElement The contenteditable element (role=“textbox”, aria-multiline=“true”). Do not mutate its DOM.
value string Current value in valueFormat (‘’ when empty in html/markdown).
name string | null Form field name.
label string Label text.
disabled boolean Disabled state.
formatState RichTextEditorFormatState Formatting at the selection: { marks, block, align, link, canUndo, canRedo }, for custom toolbars.
validity FieldValidity { valid, error, message }; computed on every change.
dirty boolean Content differs from the initial value.
touched boolean Focus has left the control at least once.
Method Description
getHTML(): string Content as sanitized HTML (‘’ when empty; plain text in plain mode).
setHTML(html: string): void Replace the content from HTML (sanitized); no events; clears history and an external custom error.
getMarkdown(): string Content as Markdown (‘’ when empty; plain text in plain mode).
setMarkdown(markdown: string): void Replace the content from Markdown; no events; clears history and an external custom error.
getText(): string Plain text; blocks are separated by a line break.
setText(text: string): void Replace the content from plain text (one paragraph per line); no events; clears history.
setValue(value: string): void Replace the content from a value in valueFormat; no events; clears history and an external custom error.
command(item: Exclude<RichTextToolbarItem, '|'>): void Run a toolbar command at the selection (for custom toolbars). No-op when disabled or read-only; formatting commands are no-ops in plain mode.
undo(): boolean Undo one step; returns false when there is nothing to undo. Emits aui-input.
redo(): boolean Redo one step; returns false when there is nothing to redo. Emits aui-input.
insertText(text: string): void Insert text at the selection (replacing it) with the current style, as an edit (emits aui-input).
openLinkEditor(): void Open the link panel for the selection or the link around the caret (Mod+K); focuses the address field.
update(options: Partial<RichTextEditorOptions>): void Merge and apply options (validated first). value replaces the content.
reportValidity(): boolean Show messages now and return validity.
setCustomError(message: string | null): void External error (server, cross-field rule); cleared when the user edits. null or ‘’ clears it.
getFormValue(): string Value for form serialization (same as value).
focus(): void Focus the content and restore the last selection.
reset(): void Restore the initial value; clear history and dirty/touched/submitted state.
destroy(): void Remove rendering, listeners, form registration, and subscriptions.
Event Detail Bubbles Cancelable Description
aui-input RichTextEditorValueDetail ({ value: string; validity: FieldValidity }) Yes No The user edited the content (typing, deletion, formatting, paste, drop, cut, undo, redo).
aui-change RichTextEditorValueDetail ({ value: string; validity: FieldValidity }) Yes No Focus left the component (host) and the content differs from when it was focused.

All rules live in the aui.components cascade layer, so application styles override them without specificity battles.

Class Purpose
aui-field aui-rte Host.
aui-rte--invalid | --disabled | --readonly | --plain | --empty State modifiers on the host.
aui-field__label | __required | __description | __counter | __error Shared field chrome.
aui-rte__toolbar role=“toolbar” container (hidden in plain mode).
aui-rte__tool | aui-rte__tool--text | aui-rte__tool--active Toolbar button (data-command=item); –text: text label (P, H1-H3); –active: link tool while the caret is in a link.
aui-rte__separator Toolbar separator (role=“separator”).
aui-rte__link-panel | __link-label | __link-row | __link-input | __link-button | __link-apply | __link-remove | __link-cancel | __link-error Link editor panel.
aui-rte__surface Bordered, scrolling container of the content; the control boundary and focus indicator.
aui-rte__content The contenteditable textbox.
aui-rte__block | aui-rte__code | aui-rte__list | aui-rte__link Rendered blocks, code block, list containers, and links inside the content.
aui-rte__align-center | aui-rte__align-end | aui-rte__align-justify Alignment classes on rendered blocks inside the editor (the HTML output uses data-align instead).
Slot Default icon
error alert-circle

Tokens this stylesheet reads (verified in CI). Override them in a theme or with the Theme Designer.

--aui-border-width | --aui-color-border | --aui-color-border-strong | --aui-color-disabled-bg | --aui-color-error-fg | --aui-color-focus-ring | --aui-color-hover | --aui-color-link | --aui-color-on-primary | --aui-color-primary | --aui-color-primary-hover | --aui-color-selected | --aui-color-selected-text | --aui-color-surface | --aui-color-surface-sunken | --aui-color-text | --aui-color-text-disabled | --aui-color-text-muted | --aui-control-height-md | --aui-control-padding-x | --aui-focus-ring-offset | --aui-focus-ring-width | --aui-font-family-mono | --aui-font-family-sans | --aui-font-size-2xl | --aui-font-size-lg | --aui-font-size-md | --aui-font-size-sm | --aui-font-size-xl | --aui-font-weight-medium | --aui-font-weight-semibold | --aui-icon-size-sm | --aui-line-height-normal | --aui-line-height-tight | --aui-radius-md | --aui-radius-sm | --aui-space-1 | --aui-space-2 | --aui-space-3 | --aui-touch-target-min

type RichTextMode = 'rich' | 'plain';
type RichTextValueFormat = 'html' | 'markdown' | 'text';
type RichTextToolbarItem =
| 'paragraph' | 'heading1' | 'heading2' | 'heading3'
| 'bold' | 'italic' | 'underline' | 'strike' | 'code' | 'link'
| 'bulletList' | 'orderedList' | 'quote' | 'codeBlock'
| 'alignStart' | 'alignCenter' | 'alignEnd' | 'alignJustify'
| 'undo' | 'redo' | 'clearFormatting' | '|';
interface RichTextEditorConfig { validationTiming: ValidationTiming; markdownShortcuts: boolean; historyLimit: number }
interface RichTextEditorValueDetail { value: string; validity: FieldValidity }
interface RichTextEditorFormatState {
marks: RichTextMark[];
block: 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'quote' | 'codeBlock' | 'bulletList' | 'orderedList';
align: RichTextAlign;
link: string | null;
canUndo: boolean;
canRedo: boolean;
}
type RichTextMark = 'bold' | 'italic' | 'underline' | 'strike' | 'code';
type RichTextAlign = 'start' | 'center' | 'end' | 'justify';
type RichTextHeadingLevel = 1 | 2 | 3;
function richTextFromHtml(html: string, headings?: readonly RichTextHeadingLevel[], doc?: Document): RichTextDocument;
function richTextToHtml(doc: RichTextDocument): string;
function richTextFromMarkdown(markdown: string, validateUrl: (url: string) => string | null, headings?: readonly RichTextHeadingLevel[]): RichTextDocument;
function richTextToMarkdown(doc: RichTextDocument): string;

RichTextDocument, RichTextBlock, and RichTextRun describe the model (a flat list of blocks with runs of marked text).

Pattern: Multi-line textbox (contenteditable, role=textbox, aria-multiline) with an associated WAI-ARIA toolbar (roving tabindex) and a labelled link editor group.

Key Context Action
Tab / Shift+Tab Outside a list item Moves focus between the toolbar (one tab stop), the content, and the rest of the page (native).
Tab / Shift+Tab Selection entirely in list items Indents / outdents the list items (depth 0-4).
Escape, then Tab / Shift+Tab Content Leaves the editor instead of indenting (no keyboard trap).
Enter Content Splits the block into a new paragraph or list item; at the end of a heading starts a paragraph; in an empty list item outdents or leaves the list; in an empty quote leaves it; in a code block inserts a line break and on an empty last line leaves it; after a line of ``` (Markdown shortcuts) creates a code block.
Shift+Enter Content Inserts a line break within the block (browser insertLineBreak).
Backspace Start of a block List item: outdents, then becomes a paragraph; heading, quote, or empty code block: becomes a paragraph; otherwise merges with the previous block.
Backspace / Delete and their word/line variants (for example Ctrl+Backspace) Content Deletes a grapheme cluster, word, or line (as the browser reports it) through the model; Delete at the end of a block merges the next block.
Mod+B / Mod+I / Mod+U Content Toggles bold / italic / underline on the selection, or for the next typed text when the selection is collapsed.
Mod+Shift+X / Mod+E Content Toggles strikethrough / inline code.
Mod+K Content Opens the link panel for the selection or the link at the caret.
Mod+Shift+7 / Mod+Shift+8 Content Toggles numbered / bulleted list.
Mod+Alt+0 / Mod+Alt+1 / Mod+Alt+2 / Mod+Alt+3 Content Normal text / heading 1 / heading 2 / heading 3 (heading toggles back to normal text).
Mod+Shift+L / Mod+Shift+E / Mod+Shift+R / Mod+Shift+J Content Align start / center / end / justify.
Mod+Z Content Undo.
Mod+Shift+Z / Mod+Y Content Redo.
Alt+F10 Content (rich mode) Moves focus to the toolbar (its current tab-stop tool).
ArrowLeft / ArrowRight Toolbar Previous / next tool in DOM order, wrapping (roving tabindex).
Home / End Toolbar First / last tool.
Enter / Space Toolbar tool Runs the command and returns focus to the content (the Link tool opens the link panel).
Escape Toolbar Returns focus to the content.
Enter Link address Applies the link (or shows the error when the URL policy rejects it); focus returns to the content.
Escape Link panel address Closes the panel without changes; focus returns to the content.
Tab / Shift+Tab Link panel Moves between the address, Apply, Remove link, and Cancel buttons (native).
Element Role
content (.aui-rte__content) textbox (aria-multiline=true)
toolbar (.aui-rte__toolbar) toolbar (aria-label “Formatting”, aria-controls = content id)
toolbar tools button (native); toggle tools carry aria-pressed
toolbar separators separator (aria-orientation=vertical)
link panel (.aui-rte__link-panel) group (aria-label “Link”)
link address textbox (native input type=url with <label for>)
icons none (aria-hidden=true)
Attribute Element Value
aria-labelledby content Id of the visible label
aria-describedby content Help text, counter, and visible error message ids
aria-invalid content “true” only while an error message is shown
aria-required content “true” when required
aria-readonly content “true” when readOnly
aria-disabled content “true” when disabled (the content is also removed from the tab order)
aria-placeholder content The placeholder text, when set
aria-pressed toggle tools (block types, marks, lists, quote, code block, alignment) “true” when the format applies at the selection
aria-disabled toolbar tools “true” when read-only/disabled, nothing to undo/redo, or marks/link inside a code block (tools stay focusable)
aria-label toolbar tools Localized command name (also the title tooltip)
aria-invalid / aria-describedby link address Set when the URL policy rejects the address; describes it with the link error message

The surface shows the focus indicator when the content has :focus-visible (border and 1px ring in color-focus-ring). Toolbar tools and link panel buttons use a focus-ring-width outline in color-focus-ring. Focus returns to the content with the previous selection after toolbar commands and when the link panel closes.

Criterion Level Note
1.3.1 Info and Relationships A
1.4.1 Use of Color A
1.4.3 Contrast (Minimum) AA
1.4.10 Reflow AA
1.4.11 Non-text Contrast AA
2.1.1 Keyboard A
2.1.2 No Keyboard Trap A
2.4.3 Focus Order A
2.4.7 Focus Visible AA
2.5.8 Target Size (Minimum) AA
3.3.1 Error Identification A
3.3.2 Labels or Instructions A
3.3.3 Error Suggestion AA
4.1.2 Name, Role, Value A
4.1.3 Status Messages AA

Automated test results: Accessibility and compliance.

  • Mod is Ctrl on Windows and Linux and Cmd on Apple platforms.
  • The required asterisk is aria-hidden; required state is conveyed by aria-required.
  • Errors appear after focus leaves the component by default, then update live.
  • Formatting changes from toolbar clicks, block type changes, undo/redo, and link add/remove are announced through a polite live region.
  • Pressed tools use selected colors plus a border, so the state is not conveyed by color alone.
  • Clicking the label focuses the content; toolbar mouse presses keep the editor’s selection.
  • axe WCAG 2.2 AA passes in the default, dark, and high-contrast themes with the link panel open and the invalid state shown (e2e).
  • Screen-reader behavior (reading of formatted content, toolbar state, announcements) has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • IME composition and mobile virtual keyboards have been tested only in Chromium.
  • Toolbar arrow keys are not mirrored in right-to-left layouts.
  • The character counter is not announced through a live region; it is referenced by aria-describedby.

default, focus-visible, empty (placeholder), filled, disabled, read-only, required, invalid (message shown), over limit, plain mode, link panel open, toolbar tool pressed, toolbar tool disabled.

From Trigger To
pristine user edits dirty; aui-input; message shown only with validationTiming: 'input'
any focus leaves the host touched; aui-change if the content changed since focus; messages shown (blur timing)
any reportValidity() (for example on form submit) submitted; messages shown for every timing
invalid (custom error) user edits custom error cleared, validity recomputed
editing Mod+K or Link tool link panel open, focus in the address field
link panel open Apply / Enter with an allowed URL link set (or the address inserted as linked text when nothing is selected); panel closed; focus back in the content
link panel open Apply with a rejected URL panel stays open; aria-invalid and the error message on the address
link panel open Remove link link removed from the range; panel closed
link panel open Cancel / Escape panel closed without changes
rich update({ mode: 'plain' }) content converted to plain text; toolbar hidden
any reset() initial value; pristine, untouched; history cleared
  • The required asterisk is aria-hidden; aria-required conveys the state.
  • Errors appear after focus leaves the component by default, then update live.
  • Toolbar mark toggles, block type changes, undo/redo, and link add/remove are announced through a polite live region.
  • Pressed tools use selected colors plus a border, so state is not conveyed by color alone. Toolbar tools are at least touch-target-min in size.
  • Focus returns to the content with its previous selection after toolbar commands and when the link panel closes. Mouse presses on tools keep the editor’s selection.
  • axe WCAG 2.2 AA passes in the default, dark, and high-contrast themes with the link panel open and the invalid state shown (e2e).
  • Manual screen-reader verification is pending (see Known limitations).

Full width of its container. The toolbar wraps onto several lines; the editor reflows at 320 CSS px without horizontal scrolling (e2e). The content is at least rows rows high and scrolls beyond maxRows; row bounds use lh units so they follow density tokens.

Logical properties throughout. The content element gets dir="rtl" when the resolved direction is 'rtl' and dir="auto" otherwise. Alignment is stored as start/center/end/justify, so it follows the writing direction; formatJustifyLeft/formatJustifyRight input events map to end/start in RTL. Toolbar arrow keys follow DOM order and are not mirrored in RTL.

Colors come from role tokens only (light, dark, and high-contrast themes). In forced-colors mode, pressed tools use Highlight/HighlightText, disabled tools GrayText, the surface border CanvasText, focus a Highlight outline, and links LinkText.

configureAbsoluteUI({ components: { richTextEditor: { validationTiming: 'input', markdownShortcuts: false, historyLimit: 50 } } });

Changes apply to existing instances immediately.

Instance options override global and theme defaults. All options can be changed with update(). Changing toolbar, mode, headings, or locale rebuilds the toolbar.

icons.error (default alert-circle). Toolbar icons are fixed.

  • No innerHTML and no document.execCommand: the view builds the DOM with createElement and text nodes from the model.
  • HTML input (value, setHTML, paste, drop) is parsed only through the core sanitizer (inert parse, allowlist rebuild) and then mapped to the model, so only supported structure survives. Scripts, event handlers, style attributes, images, and unknown elements are dropped (text is kept where applicable).
  • HTML output is generated from the model with escaping and contains only p, h1-h3, blockquote, pre/code, ul/ol/li, strong, em, u, s, code, br, and a[href]. Alignment is written as data-align="center|end|justify"; style attributes are never produced, so the output renders under a strict CSP.
  • URL policy for links (link panel, HTML, Markdown, paste): relative URLs plus linkSchemes (default http, https, mailto, tel). javascript, data, and vbscript can never be allowed (linkSchemes containing them throws). Rejected links keep their text only; the link panel shows an error.
  • Label, help text, messages, and toolbar labels are rendered with textContent. Validation messages never include the entered value.
  • Sizing custom properties are set through the CSSOM. Tested under a strict CSP with Trusted Types while typing, pasting HTML, and linking (e2e).

No persistence, network access, or telemetry. When name is set, a hidden input carries the value (in valueFormat) for native form submission, and Form reads it through getFormValue(). Copy and cut write text/plain and, in rich mode, text/html generated from the model.

Each edit produces a new immutable document; the view reuses the element of every unchanged block, so only changed blocks are rebuilt. Toolbar state is synchronized once per animation frame. History stores document snapshots (structurally shared), bounded by historyLimit; typing or deleting within 1 s coalesces into one step.

Metric Budget
RichTextEditor.create with 200 paragraphs < 80 ms in headless Chromium (e2e performance test)
Typing a character in a 200-paragraph document < 16 ms in headless Chromium (e2e performance test)

Documents of a few hundred blocks. Lists nest to depth 4 (five levels). Lengths are counted in code points of the plain text, where block boundaries count as one line break.

  • Paste: HTML is sanitized and mapped to the model; without HTML, plain text is inserted (line breaks become paragraphs). Pasting into a code block inserts plain text only. In plain mode only text is pasted.
  • Dragging text inside the editor to move it is disabled; dropping external text or HTML inserts it at the drop point.
  • Marks and links are unavailable inside code blocks (tools are aria-disabled).
  • Mark toggles with a collapsed selection apply to the next typed text.
  • Applying a link with nothing selected inserts the address as linked text; Mod+K with the caret in a link edits that link.
  • Headings outside headings (from HTML, Markdown, or h4-h6) map to the deepest allowed level that is not deeper (for example h5 becomes h3), or to the first allowed level when none qualifies.
  • Consecutive quote blocks are output as one <blockquote> with several paragraphs.
  • Backspace removes a whole grapheme cluster (for example an emoji with a skin-tone modifier).
  • IME composition that replaces a selection deletes the selection through the model first; the composed text is reconciled at compositionend.
  • showCounter without maxLength, minLength > maxLength, maxRows < rows, an empty headings, or unknown toolbar items throw RICH_TEXT_EDITOR_OPTIONS.
Code Category Cause
RICH_TEXT_EDITOR_OPTIONS configuration Missing label; invalid value, mode, valueFormat, lengths, toolbar, headings, linkSchemes (including javascript/data/vbscript), rows, maxRows, boolean/string options, or validate; or showCounter without maxLength.
Validation (validity.error) validation value-missing, too-short, too-long, custom.

A failed construction leaves the host untouched (no rendering, listeners, or subscriptions). Unsupported editing input types (for example font color or horizontal rule) are ignored.

  • No tables, images, colors, fonts, or font sizes; such content is dropped on paste.
  • Headings are limited to levels 1-3.
  • Moving text by dragging inside the editor is disabled.
  • Markdown is a CommonMark subset: ATX headings, paragraphs, hard breaks, block quotes, fenced code, nested bullet and ordered lists, emphasis, strong, strike, code spans, links, autolinks, and escapes. Underline is written and read as <u>...</u> (the only inline HTML recognized); alignment has no Markdown form and is dropped; thematic breaks are read as paragraph separators.
  • Switching from rich to plain mode discards formatting.
  • Toolbar arrow keys are not mirrored in RTL.
  • IME composition and mobile virtual keyboards have been tested only in Chromium.
  • Screen-reader behavior has not yet been manually verified with NVDA, JAWS, and VoiceOver.
  • Tables, images, horizontal rules, colors, fonts, and inline styles.
  • Mentions, collaborative editing, comments, or track changes.
  • Syntax highlighting in code blocks.
  • Custom block or mark types.