RTEToolbar
- Stable
- WCAG 2.2 evidence
- RTL
- Web · iOS · Android
The formatting bar of a rich-text editor. Built on
React Aria useToolbar (role toolbar) with a roving tab index: the bar is a single tab stop, Left and Right arrows
move between tools (mirrored in right-to-left locales), Home and End jump to the first and last tool, and Tab leaves
the bar from wherever focus is; coming back restores the last focused tool (WCAG 2.1.1, 2.4.3, 4.1.2). Disabled tools
are skipped. On the web the tools wrap onto a second row in narrow containers instead of scrolling sideways (1.4.10). The library ships no editor engine: compose the tools and wire their state to your editor. Consumer duties: the
toolbar name, tool names and shortcut hints.
import { RTEToolbar } from "@nexera-ui/react";- 6
- examples
- 8
- props
- 3
- live controls
- 3
- platforms
- 5
- 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 { RTEToolbar } from "@nexera-ui/react";
<RTEToolbar />Examples 5
The same examples as Storybook, rendered live. Open Code to copy one.
Compact
Figma Type=Compact.
import { useState, type ReactNode } from "react";
import { RTETool, RTEToolbar, RTEToolbarDivider } from "@nexera-ui/react";
import {
LuAtSign,
LuBold,
LuItalic,
LuLink,
LuList,
LuListOrdered,
LuStrikethrough,
} from "react-icons/lu";
function useMarks(initial: readonly string[]) {
const [marks, setMarks] = useState<ReadonlySet<string>>(new Set(initial));
const toggle = (key: string) => (selected: boolean) => {
setMarks((previous) => {
const next = new Set(previous);
if (selected) next.add(key);
else next.delete(key);
return next;
});
};
const align = (key: string) => () => {
setMarks((previous) => {
const next = new Set([...previous].filter((mark) => !mark.startsWith("align-")));
next.add(key);
return next;
});
};
return { marks, toggle, align };
}
function Mark({
name,
label,
icon,
keys,
marks,
toggle,
}: {
name: string;
label: string;
icon: ReactNode;
keys?: string;
marks: ReadonlySet<string>;
toggle: (key: string) => (selected: boolean) => void;
}) {
return (
<RTETool
aria-label={label}
icon={icon}
isSelected={marks.has(name)}
onChange={toggle(name)}
{...(keys === undefined
? {}
: { shortcut: `Ctrl+${keys}`, "aria-keyshortcuts": `Control+${keys}` })}
/>
);
}
function CompactTools() {
const { marks, toggle } = useMarks([]);
const shared = { marks, toggle };
return (
<>
<Mark name="bold" label="Bold" icon={<LuBold />} keys="B" {...shared} />
<Mark name="italic" label="Italic" icon={<LuItalic />} keys="I" {...shared} />
<Mark
name="strike"
label="Strikethrough"
icon={<LuStrikethrough />}
keys="Shift+X"
{...shared}
/>
<RTEToolbarDivider />
<Mark name="bullets" label="Bullet list" icon={<LuList />} keys="Shift+8" {...shared} />
<Mark
name="numbers"
label="Numbered list"
icon={<LuListOrdered />}
keys="Shift+7"
{...shared}
/>
<RTEToolbarDivider />
<RTETool
kind="action"
aria-label="Link"
icon={<LuLink />}
shortcut="Ctrl+K"
aria-keyshortcuts="Control+K"
/>
<RTETool kind="action" aria-label="Mention" icon={<LuAtSign />} />
</>
);
}
export function Compact() {
return (
<RTEToolbar aria-label="Formatting" type="full">
<CompactTools />
</RTEToolbar>
);
}
Platforms
Figma RTE toolbar Mobile: the keyboard accessory bar on iOS (bg/sunken, 50 pt tools) and Android (bg/surface, 52 dp tools). One row; tools that do not fit scroll inside the bar.
import { useState, type ReactNode } from "react";
import { RTETool, RTEToolbar, RTEToolbarDivider } from "@nexera-ui/react";
import {
LuAtSign,
LuBold,
LuItalic,
LuKeyboard,
LuLink,
LuList,
LuListOrdered,
LuUnderline,
} from "react-icons/lu";
function useMarks(initial: readonly string[]) {
const [marks, setMarks] = useState<ReadonlySet<string>>(new Set(initial));
const toggle = (key: string) => (selected: boolean) => {
setMarks((previous) => {
const next = new Set(previous);
if (selected) next.add(key);
else next.delete(key);
return next;
});
};
const align = (key: string) => () => {
setMarks((previous) => {
const next = new Set([...previous].filter((mark) => !mark.startsWith("align-")));
next.add(key);
return next;
});
};
return { marks, toggle, align };
}
function Mark({
name,
label,
icon,
keys,
marks,
toggle,
}: {
name: string;
label: string;
icon: ReactNode;
keys?: string;
marks: ReadonlySet<string>;
toggle: (key: string) => (selected: boolean) => void;
}) {
return (
<RTETool
aria-label={label}
icon={icon}
isSelected={marks.has(name)}
onChange={toggle(name)}
{...(keys === undefined
? {}
: { shortcut: `Ctrl+${keys}`, "aria-keyshortcuts": `Control+${keys}` })}
/>
);
}
function MobileTools() {
const { marks, toggle } = useMarks(["bold"]);
const shared = { marks, toggle };
return (
<>
<Mark name="bold" label="Bold" icon={<LuBold />} {...shared} />
<Mark name="italic" label="Italic" icon={<LuItalic />} {...shared} />
<Mark name="underline" label="Underline" icon={<LuUnderline />} {...shared} />
<Mark name="bullets" label="Bullet list" icon={<LuList />} {...shared} />
<Mark name="numbers" label="Numbered list" icon={<LuListOrdered />} {...shared} />
<RTETool kind="action" aria-label="Link" icon={<LuLink />} />
<RTETool kind="action" aria-label="Mention" icon={<LuAtSign />} />
<RTEToolbarDivider className="ms-auto" />
<RTETool kind="action" aria-label="Hide keyboard" icon={<LuKeyboard />} />
</>
);
}
export function Platforms() {
return (
<div className="flex flex-col gap-6">
{(["ios", "android"] as const).map((platform: Platform) => (
<div key={platform} className="flex max-w-[24.5625rem] flex-col gap-2">
<span className="text-body-caption text-secondary">{platform}</span>
<RTEToolbar aria-label={`Formatting (${platform})`} platform={platform}>
<MobileTools />
</RTEToolbar>
</div>
))}
</div>
);
}
Disabled
isDisabled (as inside a disabled RichTextEditor): every tool is disabled and the bar turns bg/sunken.
import { useState, type ReactNode } from "react";
import { RTETool, RTEToolbar, RTEToolbarDivider } from "@nexera-ui/react";
import {
LuAtSign,
LuBold,
LuItalic,
LuLink,
LuList,
LuListOrdered,
LuStrikethrough,
} from "react-icons/lu";
function useMarks(initial: readonly string[]) {
const [marks, setMarks] = useState<ReadonlySet<string>>(new Set(initial));
const toggle = (key: string) => (selected: boolean) => {
setMarks((previous) => {
const next = new Set(previous);
if (selected) next.add(key);
else next.delete(key);
return next;
});
};
const align = (key: string) => () => {
setMarks((previous) => {
const next = new Set([...previous].filter((mark) => !mark.startsWith("align-")));
next.add(key);
return next;
});
};
return { marks, toggle, align };
}
function Mark({
name,
label,
icon,
keys,
marks,
toggle,
}: {
name: string;
label: string;
icon: ReactNode;
keys?: string;
marks: ReadonlySet<string>;
toggle: (key: string) => (selected: boolean) => void;
}) {
return (
<RTETool
aria-label={label}
icon={icon}
isSelected={marks.has(name)}
onChange={toggle(name)}
{...(keys === undefined
? {}
: { shortcut: `Ctrl+${keys}`, "aria-keyshortcuts": `Control+${keys}` })}
/>
);
}
function CompactTools() {
const { marks, toggle } = useMarks([]);
const shared = { marks, toggle };
return (
<>
<Mark name="bold" label="Bold" icon={<LuBold />} keys="B" {...shared} />
<Mark name="italic" label="Italic" icon={<LuItalic />} keys="I" {...shared} />
<Mark
name="strike"
label="Strikethrough"
icon={<LuStrikethrough />}
keys="Shift+X"
{...shared}
/>
<RTEToolbarDivider />
<Mark name="bullets" label="Bullet list" icon={<LuList />} keys="Shift+8" {...shared} />
<Mark
name="numbers"
label="Numbered list"
icon={<LuListOrdered />}
keys="Shift+7"
{...shared}
/>
<RTEToolbarDivider />
<RTETool
kind="action"
aria-label="Link"
icon={<LuLink />}
shortcut="Ctrl+K"
aria-keyshortcuts="Control+K"
/>
<RTETool kind="action" aria-label="Mention" icon={<LuAtSign />} />
</>
);
}
export function Disabled() {
return (
<RTEToolbar aria-label="Formatting" isDisabled>
<CompactTools />
</RTEToolbar>
);
}
Right to left
Right-to-left: the tools run from the right, the arrow keys are mirrored, and Undo / Redo icons flip.
import {
DirectionalIcon,
RTETool,
RTEToolSelect,
RTEToolbar,
RTEToolbarDivider,
} from "@nexera-ui/react";
import { LuBold, LuItalic, LuList, LuRedo2, LuUndo2 } from "react-icons/lu";
export function RightToLeft() {
return (
<RTEToolbar aria-label="التنسيق">
<RTETool
kind="action"
aria-label="تراجع"
icon={
<DirectionalIcon>
<LuUndo2 />
</DirectionalIcon>
}
/>
<RTETool
kind="action"
aria-label="إعادة"
icon={
<DirectionalIcon>
<LuRedo2 />
</DirectionalIcon>
}
/>
<RTEToolbarDivider />
<RTEToolSelect
aria-label="نوع الكتلة"
options={[
{ id: "p", label: "فقرة" },
{ id: "h2", label: "عنوان 2" },
]}
defaultValue="p"
/>
<RTEToolbarDivider />
<RTETool aria-label="غامق" icon={<LuBold />} defaultSelected />
<RTETool aria-label="مائل" icon={<LuItalic />} />
<RTETool aria-label="قائمة نقطية" icon={<LuList />} />
</RTEToolbar>
);
}
In editor
A realistic composed editor with a plain contenteditable element (the toolbar above a plain contenteditable element, tools wired to the selection).
export function InEditor() {
return <RichTextEditorDemo />;
}
Props 8
Press "Try it" on a card to load that prop into the playground.
8 props shown
children*NexeraReactNodeThe tools, in reading order: RTETool, RTEToolSelect and RTEToolbarDivider between groups. To
push trailing tools to the end, give the first of them
className="ms-auto". Any other focusable control (an IconButton, a link) joins the arrow-key navigation too.
typeNexera"full" | "compact"Which Figma tool set this toolbar reproduces. full: undo / redo, block type, inline marks, blocks,
alignment, inserts and clear formatting (the toolbar of RichTextEditor); compact: bold, italic, strikethrough,
lists, link and mention. The tools are composed by you, so the value only sets data-type: Figma has no visual
difference between the two besides the tools and the frame width.
platformNexera"web" | "ios" | "android"Platform look. iOS and
Android render the keyboard accessory bar: one row of 50 pt / 52 dp tools that scrolls sideways when they do not fit.
The tools inside follow it. Defaults to the NexeraProvider platform.
isDisabledNexerabooleanDisables every tool (for example while the editor is disabled); the bar turns bg/sunken. Defaults to the enclosing
RichTextEditor's isDisabled.
classNameNexerastringExtra classes, merged last so they win over the defaults.
styleNexeraCSSPropertiesInline style.
aria-labelNexerastringAccessible name ("Formatting", "Text formatting"). Translate it.
Accessible name; aria-labelledby wins when both are set.
aria-labelledbyNexerastringId(s) of element(s) that name the toolbar; wins over aria-label.
Id(s) of the visible element(s) that name the toolbar.
* 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
4 direct · 1 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.4.10ReflowLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.4.3Focus OrderLevel 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-disabled
<RTEToolbar 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.