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.

KeyAction
Ctrl/Cmd + BToggle bold
Ctrl/Cmd + IToggle italic
Ctrl/Cmd + UToggle underline
Ctrl/Cmd + ZUndo
Ctrl/Cmd + Shift + ZRedo
Ctrl/Cmd + YRedo (alternative)
EnterNew line; splits list items in lists
TabIndent list item or move to next table cell
Shift + TabOutdent 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.

KeyAction
TabMove focus between toolbar buttons
Enter / SpaceActivate the focused button
EscapeClose 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.

KeyAction
Arrow DownMove to the next menu item (wraps around)
Arrow UpMove to the previous menu item (wraps around)
EnterActivate the focused menu item
EscapeClose the menu
Type-aheadFilter 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.

KeyAction
Arrow RightOpen submenu for the focused item
Arrow LeftClose submenu and return to parent menu
EscapeClose submenu and return to parent menu
Arrow Down / Arrow UpNavigate submenu items
EnterActivate 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">.

KeyAction
Enter / SpaceOpen the heading menu from the rail
Arrow DownOpen the heading menu and focus first
Arrow Down / Arrow UpNavigate heading entries inside the menu
EnterScroll to the focused heading
EscapeClose the menu

#Table Editing

Tables support full keyboard navigation between cells. Arrow keys move between cells when the cursor reaches a cell boundary.

KeyAction
TabMove to the next cell
Shift + TabMove to the previous cell
Arrow UpMove to the cell above (when cursor is on the first line of a cell)
Arrow DownMove to the cell below (when cursor is on the last line of a cell)
Arrow LeftMove to the cell on the left (when cursor is at the start of a cell)
Arrow RightMove to the cell on the right (when cursor is at the end of a cell)
Shift + ArrowExtend multi-cell selection in the arrow direction
EscapeClear multi-cell selection
Delete / BackspaceClear contents of all selected cells

#Block Mode

In block mode, each content block is an independent editable region. Additional keyboard behavior:

KeyAction
Arrow UpMove to the previous block (when cursor is at the start of a block)
Arrow DownMove to the next block (when cursor is at the end of a block)
Ctrl/Cmd + AFirst press selects the current block; second press selects all blocks
/Opens the slash command menu (when enabled)
BackspaceOn 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.

ElementUsage
<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" regionRich text editing area

#ARIA Attributes

AttributeUsage
role="textbox" + aria-multilineApplied to the editing region
aria-label / aria-labelledbyAccessible name for the editing region (via ariaLabel/ariaLabelledby)
aria-readonlySet 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-activedescendantSet 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-orientationApplied to <TextEditor.Toolbar> and the floating context toolbar
aria-labelApplied to all icon-only toolbar buttons, block controls, and table controls
aria-pressedApplied 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 activeIndex that highlights the current item via data-p-focus as 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.