Rich Text Editor
- Stable
- WCAG 2.2 evidence
- RTL
The shell of a rich-text field: label, an editor box with a formatting toolbar and a content
slot, and a footer with helper text or an error and a counter. The library ships **no editor engine**: you render the
editable element through children and wire its content and the tools' state; the shell never inserts content. Wiring the content slot (WCAG 1.3.1, 4.1.2): spread contentProps on your editable element. They give it
role="textbox", aria-multiline="true", the label (aria-labelledby to the visible label, or aria-label), the
helper / error / counter (aria-describedby), aria-invalid, aria-required, aria-disabled, aria-readonly and
aria-placeholder, plus the content typography. Make the element non-editable while disabled or read-only. A click on
the label or on the content padding focuses it. Focus in the content draws the brand border and halo on the editor box (2.4.7). Errors are text with an icon under the
field, linked to the text box (1.4.1, 3.3.1); on a failed submit, move focus to the text box so the error is read with
it. Paste is never blocked (3.3.7, 3.3.8). The label, toolbar and text wrap (1.4.10, 1.4.12). Consumer duties: label, helper, error and counter wording; validation; the editor engine, its keyboard shortcuts and
its accessible output (headings, lists).
import { RichTextEditor } from "@nexera-ui/react";- 4
- examples
- 16
- props
- 8
- live controls
- 1
- platform
- 10
- WCAG criteria
- 1
- block uses it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { RichTextEditor } from "@nexera-ui/react";
<RichTextEditor />Examples 3
The same examples as Storybook, rendered live. Open Code to copy one.
Filled
Figma Content=Filled: a heading, a paragraph and a bullet list in the Figma typography.
const FILLED_HTML =
'<h2 class="m-0 text-heading-h3">Open enrollment starts on 1 November</h2><p class="m-0 mt-3">This year you can choose between two health plans and add family members until 30 November. Read the benefits guide before you pick a plan.</p><ul class="m-0 mt-3 list-disc ps-6"><li>Compare the Standard and Plus plans</li><li>Add your spouse or children</li><li>Submit before 30 November</li></ul>';
export function Filled() {
return <RichTextEditorDemo initialHtml={FILLED_HTML} />;
}
Error and disabled
Figma State=Error (isInvalid + errorMessage, icon and text) and State=Disabled (isDisabled).
import { RTETool, RTEToolbar, RTEToolbarDivider, RichTextEditor } from "@nexera-ui/react";
import { LuBold, LuItalic, LuList } from "react-icons/lu";
const PLACEHOLDER = "Write your announcement. Type / for blocks, @ to mention.";
const FILLED_HTML =
'<h2 class="m-0 text-heading-h3">Open enrollment starts on 1 November</h2><p class="m-0 mt-3">This year you can choose between two health plans and add family members until 30 November. Read the benefits guide before you pick a plan.</p><ul class="m-0 mt-3 list-disc ps-6"><li>Compare the Standard and Plus plans</li><li>Add your spouse or children</li><li>Submit before 30 November</li></ul>';
function SimpleToolbar() {
return (
<RTEToolbar aria-label="Formatting">
<RTETool aria-label="Bold" icon={<LuBold />} />
<RTETool aria-label="Italic" icon={<LuItalic />} />
<RTEToolbarDivider />
<RTETool aria-label="Bullet list" icon={<LuList />} />
</RTEToolbar>
);
}
export function ErrorAndDisabled() {
return (
<div className="flex max-w-[55rem] flex-col gap-8">
<RichTextEditor
label="Announcement"
description="Sent to all 128 employees by email and in the app."
errorMessage="Add a message before publishing."
isInvalid
counter="0 / 5,000"
placeholder={PLACEHOLDER}
toolbar={<SimpleToolbar />}
>
{({ contentProps }) => (
<div {...contentProps} contentEditable suppressContentEditableWarning />
)}
</RichTextEditor>
<RichTextEditor
label="Announcement"
description="Sent to all 128 employees by email and in the app."
counter="312 / 5,000"
content="filled"
isDisabled
toolbar={<SimpleToolbar />}
>
{({ contentProps, isDisabled }) => (
<div
{...contentProps}
contentEditable={!isDisabled}
suppressContentEditableWarning
dangerouslySetInnerHTML={{ __html: FILLED_HTML }}
/>
)}
</RichTextEditor>
</div>
);
}
Long label right to left
A long label and helper text wrap; right-to-left mirrors the toolbar, the footer and the text.
import { RTETool, RTEToolbar, RTEToolbarDivider, RichTextEditor } from "@nexera-ui/react";
import { LuBold, LuItalic, LuList } from "react-icons/lu";
export function LongLabelRightToLeft() {
return (
<RichTextEditor
className="max-w-[55rem]"
label="إعلان لجميع الموظفين في جميع المكاتب، يُترجم إلى كل اللغات المدعومة قبل النشر"
description="يُرسل إلى جميع الموظفين البالغ عددهم 128 عبر البريد الإلكتروني وفي التطبيق."
counter="0 / 5,000"
placeholder="اكتب إعلانك هنا."
toolbar={
<RTEToolbar aria-label="التنسيق">
<RTETool aria-label="غامق" icon={<LuBold />} />
<RTETool aria-label="مائل" icon={<LuItalic />} />
<RTEToolbarDivider />
<RTETool aria-label="قائمة نقطية" icon={<LuList />} />
</RTEToolbar>
}
>
{({ contentProps }) => (
<div {...contentProps} contentEditable suppressContentEditableWarning />
)}
</RichTextEditor>
);
}
Props 16
Press "Try it" on a card to load that prop into the playground.
16 props shown
children*Nexera(props: RichTextEditorRenderProps) => ReactNodeRenders the editable content (the slot under the toolbar). The library ships no editor engine: render your own
contenteditable element or your engine's root with contentProps on it, and keep its content and the tools' state
yourself. The shell never inserts or changes content.
toolbarNexeraReactNodeThe formatting toolbar: usually an RTEToolbar. It is
disabled with the editor.
descriptionNexeraReactNodeHelper text under the editor, linked with aria-describedby. Hidden while
an error message is shown.
errorMessageNexeraReactNodeError text under the editor, shown with an icon while isInvalid and linked with
aria-describedby (WCAG 1.4.1, 3.3.1). Say what is wrong and how to fix it.
counterNexeraReactNodeCharacter count at the end of the footer, for example "312 / 5,000"; linked with
aria-describedby. Announce limit warnings yourself (announceStatus), not every keystroke.
placeholderNexerastringExample text drawn over the empty content while content="empty", and exposed as
aria-placeholder. Never the only label.
contentNexera"empty" | "filled"Whether the content is empty: the placeholder shows only while "empty". Keep it in sync with your
editor.
isInvalidNexerabooleanInvalid: danger border, the error message instead of the helper text, aria-invalid.
isDisabledNexerabooleanDisabled: dimmed label and text, sunken toolbar with every tool disabled, aria-disabled.
Also make your element non-editable.
isReadOnlyNexerabooleanRead-only: aria-readonly on the text box. Make your element non-editable.
isRequiredNexerabooleanRequired: aria-required on the text box. Validate on submit and set isInvalid with an errorMessage.
classNameNexerastringExtra classes for the root, merged last so they win over the defaults.
styleNexeraCSSPropertiesInline style of the root.
labelNexeraFieldLabelContentVisible label, linked to the control by React Aria (WCAG 1.3.1, 3.3.2). Short noun, for example "Work email". Wraps instead of truncating. A placeholder is never a substitute for it. Not set: the field has no visible label. Not set: the field has no visible label of its own.
aria-labelNexerastringAccessible name when it must differ from the visible label. Prefer the visible label (WCAG 2.5.3).
Accessible name. Required when there is no visible label; translate it.
Accessible name. Optional when aria-labelledby is set.
aria-labelledbyNexerastringId(s) of element(s) that name the field; wins over the visible label.
Id(s) of element(s) that name the field; wins over aria-label.
Id(s) of visible element(s) that name the field.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
8 direct · 2 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.1Use of ColorLevel A · tested directly
- 1.4.10ReflowLevel AA · supporting test
- 1.4.12Text SpacingLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.4.3Focus OrderLevel A · tested directly
- 2.4.7Focus VisibleLevel AA · tested directly
- 3.3.1Error IdentificationLevel A · tested directly
- 3.3.2Labels or InstructionsLevel A · tested directly
- 4.1.2Name, Role, ValueLevel A · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
data-disableddata-focus-withindata-hovereddata-invalid
<RichTextEditor className="data-disabled:opacity-90 shadow-sm" />Used in blocks
Related components
- CalendarDayOne day of a {@link CalendarMonth } grid, built on React Aria `CalendarCell`: a `gridcell` whose button is named by the full, localised date ("Wednesday, October 14, 2026"), with `aria-selected`, `aria-disabled` and the "today" and range descriptions read by screen readers (WCAG 1.3.1, 4.1.2).
- CalendarMonthA month calendar for picking a date or a date range: header with previous / next buttons and the localised month name, weekday row, and six weeks of `CalendarDay`s.
- CheckboxThe bare 18 px checkbox: one independent yes/no choice applied on submit, or a row selector in a table, list or tree.
- CheckboxCardA large checkbox option with an icon, a title and a description, for a few options that need explanation (notification channels, benefits).
- CheckboxFieldA checkbox with a label and optional helper text: the box (the same element as `Checkbox`) followed by the label and description, applied on submit.
- ColorInputA hex colour field with a swatch and a picker popover.
- ColorPickerPicks a colour.
- ColorTokenCardDocumentation card for one colour token: a 96 px colour sample with an "Aa" text sample, then the name, token path, hex value and contrast note as text.