Accessibility
The editor is a composite widget: a tab strip, a ribbon, a document canvas and a status bar, plus optional side panels. Keyboard users move between those regions with F6, and within the current one with arrows or Tab.
Moving between regions
| Key | Action |
|---|---|
F6 or Ctrl+F6 (Cmd+F6 on macOS) | Next region, in document order: toolbar, side panels, document, comments, status bar, then open floating menus |
Shift+F6 or Ctrl+Shift+F6 | Previous region |
Escape | From the document, go to the toolbar; from the toolbar or status bar, return to the document |
Ctrl+/ (Cmd+/) | Open the keyboard shortcut reference, from any region |
Entering the toolbar lands on the selected tab. Other regions resume on the control you last used there.
Regions that aren’t rendered or have nothing focusable (a closed side panel, a hidden status bar) drop out of the cycle.
Ctrl+F6 exists because browsers reclaim plain F6 for their own panes once
focus leaves the page. Word for the web and Google Docs use it for the same
reason.
Escape yields to whatever is already using it. If the format painter is armed
or the search bar is open, Escape dismisses that instead of moving focus, and
inside a comment, footnote or header/footer editor it closes that editor. Only
when nothing else claims it does it move focus to the toolbar.
The toolbar
The ribbon is a single Tab stop, following the
WAI-ARIA toolbar pattern .
One Tab enters it, one Tab leaves it, and arrows move between every control
inside it, across group boundaries. Groups are labelled role="group"
elements, so a screen reader names each group as you enter it.
| Key | Action |
|---|---|
← / → | Previous / next control, wrapping at the ends |
Home / End | First / last control in the ribbon |
Tab | Leave the ribbon entirely |
Inside a text field (font size, row height, column width) the arrows belong to the caret. Once the caret reaches the start or end of the value, the next press in that direction moves to the neighbouring control.
Tabbing back into the ribbon lands on the control you last used, not the first
one. Disabled controls (disabled or aria-disabled) are skipped.
Tabs
| Key | Action |
|---|---|
← / → | Previous / next tab (the ribbon follows immediately) |
Home / End | First / last tab |
↓ | Move down into the ribbon without changing tab |
The document
| Key | Action |
|---|---|
Tab / Shift+Tab | Indent / outdent — not focus movement |
Shift+F10 or the Menu key | Open the context menu at the caret |
F6, Ctrl+F6 / Escape | Leave the document (see above) |
Because Tab indents, the document would otherwise be a keyboard trap. The
editing surface advertises its exits with aria-keyshortcuts="F6 Control+F6"
and an aria-describedby hint (“Press F6 to move to the toolbar, or Control
plus slash for all shortcuts”), which satisfies
WCAG 2.1.2 No Keyboard Trap .
The context menu opens at the caret, not at the pointer’s last position, and
Escape closes it and returns focus to the document.
The ruler is pointer-only and aria-hidden. Keyboard users set the same
values with Indent/Outdent and the Page tab’s margin controls.
Modes and read-only
The mode ("edit" | "review" | "view") is announced, not
just enforced.
In "view" the document surface keeps role="textbox" and adds
aria-readonly="true", so a screen reader says “read only” instead of offering
an edit affordance that swallows input. It also gets tabindex="0", because a
non-editable element is not focusable and F6 could not reach it otherwise.
Mode changes are spoken through a visually hidden role="status" region:
“Editing mode”, “Suggesting mode”, “Viewing mode”. A change can disable or
re-enable the whole ribbon, and this is what tells a screen-reader user why.
The mode the editor mounts with is not announced, only later changes, whether
they come from the prop, useSetEditorMode or the status bar.
The status-bar mode picker names the current mode in its trigger’s accessible name and marks it as checked in the menu. Suggesting is offered only when the track-changes plugin is mounted, or when the editor is already in review.
In view mode, reading controls keep working: footnote and endnote anchors stay operable, and “View Footnotes” / “View Endnotes” stay enabled because their panel is read-only. Controls that would change the document are disabled or removed.
Disabled semantics
Two conventions coexist, both valid under the APG :
- Ribbon controls use native
disabled. They drop out of the tab order and out of the toolbar’s arrow rail, so arrowing across the ribbon never lands on something that cannot act. - Menu items use
aria-disabled. They stay focusable, so a keyboard user browsing a menu can still read every option and hear which are unavailable.
A split button follows its trigger’s context: the trigger is natively disabled when nothing inside the menu can run, rather than opening onto a menu whose every item is unavailable.
Language
Screen readers switch voice and pronunciation on the lang attribute, so the
editor emits it at three levels:
- the editor chrome (ribbon, panels, status bar) carries the UI locale you
pass as
locale; - the document root carries the document language (
doc.attrs.language,"en-GB"by default), so content is never announced in the UI’s language; - a paragraph or heading whose effective language differs from the document’s
carries that language itself — whether it says so through
proofingLanguageor inherits it from its paragraph style’slanguage.
Only deviations are emitted. proofingLanguage defaults to null (“inherit”)
and styles set no language unless a template or a DOCX import gives them one, so
in a single-language document nothing below the root is tagged at all.
Values are normalized to BCP 47 (fr-fr → fr-FR); anything that is not a
well-formed tag is dropped rather than emitted, since a wrong tag is worse for
a screen reader than none.
What you are responsible for
The editor owns the accessibility of its own chrome. Anything above it belongs to the embedding page:
- Landmarks. The editor exposes
role="main"for the canvas and labelled regions for the ribbon, the status bar and its panels, but the surrounding page needs its ownheader,navandfooterlandmarks. Do not wrap the editor in your own<main>: a page should have only one. For the same reason, avoid mounting two full editors on one page; each renders its ownrole="main". - Skip links. If the editor sits below a long masthead, provide a “skip to editor” link. The editor does not inject one.
- Page title and language. Set
<html lang>and a meaningful document title. - Custom toolbar items. Controls you add through
uiExtensionsor a custom toolbar tab join the ribbon’s arrow navigation automatically, but you must give them an accessible name (aria-labelor visible text). Usedata-toolbar-skipon a wrapper if a control manages its own focus and should stay out of the rail. - Custom node views. Interactive node views are your own to make operable; the editor does not manage focus inside them.
Conformance
The target is WCAG 2.1 Level AA. Automated checks (axe-core) run against the editor at rest and with each toolbar tab, panel and dialog open; keyboard behaviour is covered by end-to-end tests in a real browser. Automated tooling catches roughly a third of accessibility issues, so these are a floor, not a guarantee.
Known gaps:
- The bullet-list button never reports
aria-pressed="true", even inside a bulleted list. - The slash-menu listbox and the link form’s error message use fixed DOM ids,
so two editors on one page produce duplicate ids and broken
aria-controls/aria-describedbyreferences.