Text Editor - Accessibility
Keyboard navigation, screen reader support, and focus management for the Text Editor component targeting WCAG compliance.
#Overview
TextEditor targets WCAG 2.1 AA. The component provides keyboard navigation across every interactive region, ARIA attributes for screen readers, semantic HTML, and managed focus for popover menus and context toolbars.
The component addresses the following accessibility areas:
- Keyboard navigation for editor content, toolbar, menus, and tables
- Focus management for popover menus and context toolbars
- Screen reader support via contenteditable semantics and button labels
- Platform-aware shortcuts that adapt between macOS and Windows/Linux
#Keyboard Navigation
The following sections describe the keyboard navigation pattern for each region.
#Editor Content
The editor uses a ProseMirror contenteditable region. The following shortcuts are available in both classic and block modes.
| Key | Action |
|---|---|
Ctrl/Cmd + B | Toggle bold |
Ctrl/Cmd + I | Toggle italic |
Ctrl/Cmd + U | Toggle underline |
Ctrl/Cmd + Z | Undo |
Ctrl/Cmd + Shift + Z | Redo |
Ctrl/Cmd + Y | Redo (alternative) |
Enter | New line; splits list items in lists |
Tab | Indent list item or move to next table cell |
Shift + Tab | Outdent list item or move to previous table cell |
#Toolbar
<TextEditor.Toolbar> renders above the editor. Toolbar buttons are standard <button> elements, navigable with Tab and activated with Enter or Space.
| Key | Action |
|---|---|
Tab | Move focus between toolbar buttons |
Enter / Space | Activate the focused button |
Escape | Close any open popover or dropdown |
#Menus
The Slash Menu (triggered by / in block mode) and Mention Menu (triggered by @) share the same PopoverMenu primitive, so arrow key, Enter, and Escape behavior is identical across both.
| Key | Action |
|---|---|
Arrow Down | Move to the next menu item (wraps around) |
Arrow Up | Move to the previous menu item (wraps around) |
Enter | Activate the focused menu item |
Escape | Close the menu |
| Type-ahead | Filter items by typing text after the trigger character |
#Submenus
Menu items that contain nested options open a submenu panel. Submenu navigation follows the same arrow key pattern as the parent menu.
| Key | Action |
|---|---|
Arrow Right | Open submenu for the focused item |
Arrow Left | Close submenu and return to parent menu |
Escape | Close submenu and return to parent menu |
Arrow Down / Arrow Up | Navigate submenu items |
Enter | Activate the focused submenu item |
#Context Toolbar
The context toolbar appears on text selection and closes on structural changes to avoid interfering with continued editing. When the host application places a "More" trigger inside <TextEditor.ContextToolbar>, the matching <TextEditor.ContextToolbarMore> popover handles its own Escape and click-outside dismissal.
#Navigator
<TextEditor.NavigatorTrigger> is the rail element. It is focusable (tabindex="0", role="button", aria-haspopup="menu") and opens the heading list on hover, focus, Enter, Space, or ArrowDown. The <TextEditor.NavigatorMenu> popover inherits arrow key, Enter, and Escape navigation from PopoverMenu, provided heading entries are rendered as <button role="menuitem">.
| Key | Action |
|---|---|
Enter / Space | Open the heading menu from the rail |
Arrow Down | Open the heading menu and focus first |
Arrow Down / Arrow Up | Navigate heading entries inside the menu |
Enter | Scroll to the focused heading |
Escape | Close the menu |
#Table Editing
Tables support full keyboard navigation between cells. Arrow keys move between cells when the cursor reaches a cell boundary.
| Key | Action |
|---|---|
Tab | Move to the next cell |
Shift + Tab | Move to the previous cell |
Arrow Up | Move to the cell above (when cursor is on the first line of a cell) |
Arrow Down | Move to the cell below (when cursor is on the last line of a cell) |
Arrow Left | Move to the cell on the left (when cursor is at the start of a cell) |
Arrow Right | Move to the cell on the right (when cursor is at the end of a cell) |
Shift + Arrow | Extend multi-cell selection in the arrow direction |
Escape | Clear multi-cell selection |
Delete / Backspace | Clear contents of all selected cells |
#Block Mode
In block mode, each content block is an independent editable region. Additional keyboard behavior:
| Key | Action |
|---|---|
Arrow Up | Move to the previous block (when cursor is at the start of a block) |
Arrow Down | Move to the next block (when cursor is at the end of a block) |
Ctrl/Cmd + A | First press selects the current block; second press selects all blocks |
/ | Opens the slash command menu (when enabled) |
Backspace | On empty list item, lifts it out of the list; on empty block, removes it |
#Screen Reader
The editing region is exposed as an ARIA textbox (role="textbox", aria-multiline="true"), not only as a native contenteditable. This gives the region a recognized role and, when a name is provided, an accessible name. Toolbar buttons use standard <button> elements with aria-label attributes.
#Accessible name (required)
An editor with no accessible name is a WCAG 4.1.2 failure. Give every editor a name via ariaLabel or ariaLabelledby:
<TextEditor.Root v-model="value" aria-label="Comment" />
<!-- or -->
<label id="desc-label">Description</label>
<TextEditor.Root v-model="value" aria-labelledby="desc-label" />When readonly or disabled is set, the editor also exposes aria-readonly="true", and document-mutating commands are blocked even if triggered programmatically.
#Type-ahead menus (slash / mention)
While a slash or mention menu is open, the editing region becomes an ARIA combobox: it exposes role="combobox", aria-autocomplete="list", aria-expanded="true", aria-controls (pointing at the menu), and aria-activedescendant (pointing at the highlighted item). The menu itself is a role="listbox" whose entries are role="option", so screen readers announce the active option as the user arrows through it. When the menu closes the region reverts to a plain role="textbox" and focus returns to the editor.
Block, table, and navigator menus are not type-ahead comboboxes — they stay role="menu" with role="menuitem" entries.
#Semantic HTML
The component uses semantic HTML where possible.
| Element | Usage |
|---|---|
<button> | Toolbar actions, menu items |
<table>, <tr>, <td>, <th scope> | Table structure (header scope) |
<ul>, <ol>, <li> | Lists and checklists |
<h1>–<h6> | Headings |
role="textbox" region | Rich text editing area |
#ARIA Attributes
| Attribute | Usage |
|---|---|
role="textbox" + aria-multiline | Applied to the editing region |
aria-label / aria-labelledby | Accessible name for the editing region (via ariaLabel/ariaLabelledby) |
aria-readonly | Set on the editing region when readonly/disabled |
role="combobox" + aria-autocomplete="list" | Set on the editing region while a slash/mention type-ahead menu is open |
aria-expanded / aria-controls / aria-activedescendant | Set on the editing region while a type-ahead menu is open |
role="listbox" + role="option" | Slash/mention popup and its entries (block/table menus stay menu/menuitem) |
role="toolbar" + aria-label + aria-orientation | Applied to <TextEditor.Toolbar> and the floating context toolbar |
aria-label | Applied to all icon-only toolbar buttons, block controls, and table controls |
aria-pressed | Applied to toggle buttons (bold, italic, etc.) to announce state |
role="status" / aria-live="polite" | Wraps upload progress so progress and errors are announced |
#Motion
All editor animations and transitions are disabled under @media (prefers-reduced-motion: reduce).
#Labels
Icon-only toolbar buttons need an accessible label. Keyboard focus is shown via :focus-visible outlines on interactive elements within the editor. When building a custom toolbar inside <TextEditor.Toolbar>, ensure icon-only buttons have an accessible label as well.
#Focus Management
The editor content area receives focus via standard click or Tab navigation. Popover menus use tabindex="-1" to receive programmatic focus when opened, and keyboard events are captured at the document level to enable arrow key navigation within menus.
Focus behavior in menus:
- Auto-focus. When a popover menu opens, it receives focus via
popoverRef.focus(). - Active item tracking. The menu maintains an
activeIndexthat highlights the current item viadata-p-focusas the user navigates with arrow keys. - Click-outside dismissal. Clicking outside a menu closes it and returns focus to the editor.
#Focus Visible
Interactive elements use :focus-visible styles to provide visible focus indicators for keyboard users while keeping mouse interactions clean. The UI parts include this styling already; a custom toolbar should apply the same pattern to its buttons.