Skip to main content

TextFormatToolbar

Implemented

Formatting 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.

Categorymolecules
Sourcecomponents/molecules/TextFormatToolbar/TextFormatToolbar.tsx
AI HintRich-text formatting bar for contentEditable surfaces; pairs with RichTextEditor (which wires locale + toolbarLabel automatically). Commands are execCommand-style TextFormatCommand keys; the default set excludes undo/redo. Pass labels for per-command localized aria-labels (catalogs: i18n textFormatToolbar.commands.* EN/FR).

Formatting 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 via ariaLabel (default "Formatting toolbar"); every button carries its own aria-label (e.g. "Bold", "Heading 1").
  • controls wires aria-controls to the editable surface — pass the same id the surface renders with.
  • onMouseDown is preventDefault()-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 (tabIndex 0, all others -1).ArrowRight/ArrowLeft move focus among the enabled buttons with wrap-around, Home/End jump 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-min floor via min-h/min-w utilities 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.

Prop
Type
Default
Description
onCommand*
(command: TextFormatCommand) => void
—

Called with the command key when a toolbar button is activated.

commands
TextFormatCommand[]
bold…code (12)

Subset of commands to render; defaults to the compact document set (all 14 keys except undo/redo).

controls
string
—

aria-controls id of the editable surface this toolbar drives — must match that surface’s id.

disabled
boolean
false

Disables every button and sets aria-disabled on the toolbar.

ariaLabel
string
Formatting toolbar

Accessible name for the toolbar region.

labels
Partial<Record<TextFormatCommand, string>>
per-key English fallback

i18n 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.

className
string
—

Additional CSS classes appended to the root.

id
string
—

Stable root id (useId-generated by default).

Props

Preview

TextFormatToolbar

Prop
Type
Required
Default
Description
onCommand
(command: TextFormatCommand) => void
Required
—

Called with the command key when a toolbar button is activated.

commands
TextFormatCommand[]
Optional
bold, italic, underline, strikethrough, h1, h2, h3, ul, ol, link, quote, code

Optional command subset. Defaults to the 12-command compact document set; undo and redo are valid commands but opt-in only.

controls
string
Optional
—

aria-controls id of the editable surface this toolbar drives (wire it to the contentEditable element id).

disabled
boolean
Optional
false

Disables every button and sets aria-disabled on the toolbar container.

ariaLabel
string
Optional
'Formatting toolbar'

Accessible name for the toolbar (i18n key textFormatToolbar.label in the design-system catalogs).

labels
Partial<Record<TextFormatCommand, string>>
Optional
— (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.

className
string
Optional
—

Additional classes merged onto the toolbar container.

id
string
Optional
—

Override for the generated toolbar element id.

Quick use:<TextFormatToolbar />

Accessibility

ARIA roletoolbar

ARIA

2 rules

WAI-ARIA APG toolbar

role=toolbar named via ariaLabel; every command is a native <button type="button"> with its own aria-label

WAI-ARIA aria-controls

aria-controls wired via the controls prop to the editable surface id

behavior

3 rules

WAI-ARIA APG toolbar keyboard interaction

Roving tabindex: first enabled button tabIndex=0, others -1; ArrowLeft/ArrowRight move focus with wrap, Home/End jump to first/last, disabled buttons skipped

WAI-ARIA disabled state

disabled propagates to every button and reflects aria-disabled on the container

contentEditable UX convention

onMouseDown preventDefault keeps the caret inside the contentEditable surface when a button fires

a11y

1 rule

WCAG 2.5.5

Hit rect grows to --ep-tap-min (44px) while the visual button stays 32px (h-8)

Usage

Import

tsxSnippet
import { TextFormatToolbar } from "@epasslive/design-system"

Example 1

tsxSnippet
<TextFormatToolbar onCommand={(cmd) => exec(cmd)} controls="rte-surface" />

Example 2

tsxSnippet
<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

Design System v4.1.0 | ePassLive