TextFormatToolbar
ImplementedFormatting toolbar (bold/italic/underline/strikethrough/h1/h2/h3/ul/ol/link/quote/code/undo/redo) for contentEditable surfaces. Roving-tabindex keyboard nav (ArrowLeft/Right wrap, Home/End), per-command localized aria-labels via labels, 44px tap hit-rects (--ep-tap-min) with 32px visuals, Phosphor glyphs for link/quote/lists/undo/redo. Each button is a real <button> and onMouseDown preventDefault preserves the caret position.
components/molecules/TextFormatToolbar/TextFormatToolbar.tsxFormatting toolbar for contentEditable surfaces: a labelled role="toolbar" strip of glyph buttons (bold, italic, underline, strikethrough, headings, lists, link, quote, code) that reports each activation through onCommand. onMouseDown is prevented so the caret never leaves the editable surface.
TextFormatToolbar
Molecule — Rich text
Default 12-command compact document set (bold → code). Wire controls to the id of the editable surface this toolbar drives (aria-controls).
This component supports light and dark mode.
TextFormatToolbar — reduced set
Molecule — Rich text
commands={["bold", "italic", "link"]} renders a minimal comment-style toolbar. Any subset of the 14 TextFormatCommand keys is valid, in any order.
TextFormatToolbar — disabled
Molecule — Rich text
disabled={true}: every button is disabled (cursor-not-allowed, 50% opacity) and the root carries aria-disabled="true".
TextFormatToolbar — labels override
Molecule — Rich text
labels={{ … }} provides localized per-command aria-labels (Partial<Record<TextFormatCommand, string>>). Here a bold/italic/link subset carries the French catalog values (Gras, Italique, Insérer un lien) and the toolbar is named via ariaLabel="Barre de mise en forme"; unoverridden keys keep their English defaults.
Usage
import { TextFormatToolbar, type TextFormatCommand } from '@epasslive/design-system'
// Full compact set over a contentEditable surface
<TextFormatToolbar
controls={editorSurfaceId}
onCommand={(cmd: TextFormatCommand) => exec(cmd)}
/>
// Reduced comment-style set
<TextFormatToolbar
commands={['bold', 'italic', 'link']}
controls={commentSurfaceId}
onCommand={exec}
/>Pair with RichTextEditor for contentEditable surfaces — it composes this molecule and forwards its commands prop. The controls prop must match the editable surface’s id so aria-controls wiring is correct.
Commands
The 14 TextFormatCommand keys: bold, italic, underline, strikethrough, h1, h2, h3, ul, ol, link, quote, code, undo, redo. The default commands set includes all of them except undo and redo — both are valid commands but are excluded from the default compact set; pass them explicitly in a commands array to show them.
Accessibility
- Root is
role="toolbar"named viaariaLabel(default "Formatting toolbar"); every button carries its ownaria-label(e.g. "Bold", "Heading 1"). controlswiresaria-controlsto the editable surface — pass the same id the surface renders with.onMouseDownispreventDefault()-ed on each button so activation never steals focus — the caret stays in the editable surface.- Keyboard navigation follows the WAI-ARIA toolbar pattern via roving tabindex: initial focus goes to the first enabled button (
tabIndex0, all others -1).ArrowRight/ArrowLeftmove focus among the enabled buttons with wrap-around,Home/Endjump to the first/last button, and disabled buttons are skipped by the traversal. - Target size: the visual button stays 32×32px (
h-8 w-8) but the hit rectangle grows to the 44px--ep-tap-minfloor viamin-h/min-wutilities plus an inline-style belt-and-braces (the CopyButton pattern) — WCAG 2.5.8 (24px) and the 2.5.5 guideline (44px) are both satisfied. - For generic toolbar-region guidance (role, naming, landmark behavior), see TableToolbar.
Internationalization
Per-command accessible names are catalog-backed: the design-system i18n/en.json and i18n/fr.json both define all 14 keys under textFormatToolbar.commands (plus textFormatToolbar.label for the toolbar name). The labels prop is the override mechanism — pass localized strings (e.g. from next-intl) per command and any key you don’t override falls back to its English default, e.g. labels={{ bold: "Gras", link: "Insérer un lien" }}. RichTextEditor wires this for you: give it locale ("en" | "fr") and toolbarLabel and it passes the toolbar name through as the toolbar’s ariaLabel.
onCommand*(command: TextFormatCommand) => voidCalled with the command key when a toolbar button is activated.
commandsTextFormatCommand[]bold…code (12)Subset of commands to render; defaults to the compact document set (all 14 keys except undo/redo).
controlsstringaria-controls id of the editable surface this toolbar drives — must match that surface’s id.
disabledbooleanfalseDisables every button and sets aria-disabled on the toolbar.
ariaLabelstringFormatting toolbarAccessible name for the toolbar region.
labelsPartial<Record<TextFormatCommand, string>>per-key English fallbacki18n overrides for per-command aria-labels; defaults mirror the textFormatToolbar.commands keys in the design-system i18n/en.json catalog (French values in i18n/fr.json). Unoverridden keys fall back to English.
classNamestringAdditional CSS classes appended to the root.
idstringStable root id (useId-generated by default).
Props
TextFormatToolbar
onCommand(command: TextFormatCommand) => void—Called with the command key when a toolbar button is activated.
commandsTextFormatCommand[]bold, italic, underline, strikethrough, h1, h2, h3, ul, ol, link, quote, codeOptional command subset. Defaults to the 12-command compact document set; undo and redo are valid commands but opt-in only.
controlsstring—aria-controls id of the editable surface this toolbar drives (wire it to the contentEditable element id).
disabledbooleanfalseDisables every button and sets aria-disabled on the toolbar container.
ariaLabelstring'Formatting toolbar'Accessible name for the toolbar (i18n key textFormatToolbar.label in the design-system catalogs).
labelsPartial<Record<TextFormatCommand, string>>— (per-key English fallback)Per-command aria-label overrides (step-6 i18n fix). Keys not provided fall back to the English defaults, which mirror textFormatToolbar.commands.* in i18n/en.json; ready-made French values live under the same keys in i18n/fr.json — locale-aware consumers pass localized strings so buttons are not announced in English on /fr routes.
classNamestring—Additional classes merged onto the toolbar container.
idstring—Override for the generated toolbar element id.
<TextFormatToolbar />Accessibility
ARIA
2 rulesbehavior
3 rulesa11y
1 ruleUsage
Import
import { TextFormatToolbar } from "@epasslive/design-system"Example 1
<TextFormatToolbar onCommand={(cmd) => exec(cmd)} controls="rte-surface" />Example 2
<TextFormatToolbar commands={["bold", "italic", "link"]} labels={{ bold: "Gras" }} ariaLabel="Barre de mise en forme" />Anti-patterns
— Do not use it to render stored rich text — MarkdownRenderer is the display molecule
— Do not mutate the editor directly from consumer code on button press; route everything through onCommand so the toolbar stays stateless
— Do not ship locale-specific strings as the default; defaults are English, overrides come via labels