Rich Text Editor
import { RichTextEditor } from '@absoluteui/components/rich-text-editor';import '@absoluteui/components/rich-text-editor/style.css';Using Angular? See Rich Text Editor for Angular.
What it does
Section titled “What it does”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.
When to use
Section titled “When to use”- 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’).
When not to use
Section titled “When not to use”- 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.
Examples
Section titled “Examples”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);});Markdown value with a reduced toolbar
Section titled “Markdown value with a reduced toolbar”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(); });}More usage
Section titled “More usage”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).');Anatomy
Section titled “Anatomy”<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>API reference
Section titled “API reference”Factory
Section titled “Factory”RichTextEditor.create(host: HTMLElement, options: RichTextEditorOptions): RichTextEditorGlobal configuration key: richTextEditor (see Configuration).
Options
Section titled “Options”| 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, |
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. |
Properties
Section titled “Properties”| 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. |
Methods
Section titled “Methods”| 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. |
Events
Section titled “Events”| 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. |
CSS classes
Section titled “CSS classes”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). |
Icon slots
Section titled “Icon slots”| Slot | Default icon |
|---|---|
error |
alert-circle |
Design tokens
Section titled “Design tokens”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
TypeScript definitions
Section titled “TypeScript definitions”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).
Accessibility
Section titled “Accessibility”Pattern: Multi-line textbox (contenteditable, role=textbox, aria-multiline) with an associated WAI-ARIA toolbar (roving tabindex) and a labelled link editor group.
Keyboard
Section titled “Keyboard”| 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) |
ARIA attributes
Section titled “ARIA attributes”| 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.
WCAG 2.2 success criteria addressed
Section titled “WCAG 2.2 success criteria addressed”Automated test results: Accessibility and compliance.
Notes and known caveats
Section titled “Notes and known caveats”- 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.
Behavior
Section titled “Behavior”States
Section titled “States”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.
State transitions
Section titled “State transitions”| 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 |
Accessibility notes
Section titled “Accessibility notes”- The required asterisk is
aria-hidden;aria-requiredconveys 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-minin 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).
Responsive behavior
Section titled “Responsive behavior”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.
Right-to-left
Section titled “Right-to-left”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.
Theming
Section titled “Theming”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.
Global configuration
Section titled “Global configuration”configureAbsoluteUI({ components: { richTextEditor: { validationTiming: 'input', markdownShortcuts: false, historyLimit: 50 } } });Changes apply to existing instances immediately.
Component configuration
Section titled “Component configuration”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.
Security
Section titled “Security”- No
innerHTMLand nodocument.execCommand: the view builds the DOM withcreateElementand 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,styleattributes, 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, anda[href]. Alignment is written asdata-align="center|end|justify";styleattributes are never produced, so the output renders under a strict CSP. - URL policy for links (link panel, HTML, Markdown, paste): relative URLs plus
linkSchemes(defaulthttp,https,mailto,tel).javascript,data, andvbscriptcan never be allowed (linkSchemescontaining 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).
Data handling
Section titled “Data handling”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.
Performance
Section titled “Performance”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) |
Supported data scale
Section titled “Supported data scale”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.
Edge cases
Section titled “Edge cases”- 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, orh4-h6) map to the deepest allowed level that is not deeper (for exampleh5becomesh3), 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. showCounterwithoutmaxLength,minLength > maxLength,maxRows < rows, an emptyheadings, or unknown toolbar items throwRICH_TEXT_EDITOR_OPTIONS.
Errors
Section titled “Errors”| 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.
Known limitations
Section titled “Known limitations”- 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.
Unsupported scenarios
Section titled “Unsupported scenarios”- 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.