Skip to content

Accessibility ​

Editu builds on Tiptap and ProseMirror, which provide a solid foundation for accessible rich text editing. This guide covers keyboard navigation, screen reader support, and best practices for building accessible applications with Editu.

Note

Editu has not undergone a formal accessibility audit. The information below describes built-in capabilities and recommended practices but does not constitute a WCAG conformance claim.

Keyboard Navigation ​

Text Formatting ​

ShortcutAction
Mod-bBold
Mod-iItalic
Mod-uUnderline
Mod-Shift-sStrikethrough
Mod-eInline code

TIP

Mod refers to Ctrl on Windows/Linux and Cmd on macOS.

Block Elements ​

ShortcutAction
Mod-Alt-1 through Mod-Alt-3Heading levels 1–3
Mod-Shift-bBlockquote
Tab / Shift-TabIndent / outdent list items
EnterNew paragraph or continue list
Shift-Enter / Mod-EnterHard line break
--- + EnterHorizontal rule

History ​

ShortcutAction
Mod-zUndo
Mod-Shift-zRedo
Mod-yRedo (Windows alternative)

Find & Replace ​

ShortcutAction
Mod-fOpen Find
Mod-Shift-hOpen Find & Replace
EnterNext match (when Find panel is open)
Shift-EnterPrevious match
EscapeClose Find panel

Block Reordering (Drag Handle) ​

ShortcutAction
Alt-ArrowUpMove block up
Alt-ArrowDownMove block down

Table Navigation ​

ShortcutAction
TabNext cell
Shift-TabPrevious cell
Arrow keysNavigate between cells

Slash Menu ​

ShortcutAction
/Open slash command menu
ArrowUp / ArrowDownNavigate menu items
EnterSelect item
EscapeClose menu
Type to filterFilter menu items

General Navigation ​

ShortcutAction
ArrowUp / ArrowDownMove between blocks
Home / EndStart / end of line
Mod-Home / Mod-EndStart / end of document
Mod-aSelect all

Screen Reader Support ​

ARIA Attributes ​

Editu's editor element uses the contenteditable attribute, which modern screen readers recognize as an editable region. To improve screen reader support, set ARIA attributes via editorProps:

typescript
const editor = useEdituEditor({
  // Set ARIA attributes on the editor element
  editorProps: {
    attributes: {
      role: 'textbox',
      'aria-multiline': 'true',
      'aria-label': 'Document editor',
    },
  },
});

Recommended ARIA attributes:

  • role="textbox" — Identifies the editor as a text input region
  • aria-multiline="true" — Indicates that the editor supports multi-line editing
  • aria-label — Provides an accessible name for the editor

Live Regions ​

For dynamic content like save indicators and slash menu results, use ARIA live regions:

tsx
// Save indicator with live region
<div aria-live="polite" aria-atomic="true">
  {status === 'saved' && 'Content saved'}
  {status === 'saving' && 'Saving...'}
  {status === 'error' && 'Save failed'}
</div>

Announcing Content Changes ​

When you programmatically update content, announce changes to screen readers:

typescript
function announceChange(message: string) {
  const el = document.createElement('div');
  el.setAttribute('role', 'status');
  el.setAttribute('aria-live', 'polite');
  el.className = 'sr-only';
  el.textContent = message;
  document.body.appendChild(el);
  setTimeout(() => el.remove(), 1000);
}

// Usage
editor.commands.setContent(newContent);
announceChange('Document content updated');

Focus Management ​

Editor Focus ​

You can focus the editor programmatically:

typescript
// Focus at the end of the document
editor.commands.focus('end');

// Focus at the start
editor.commands.focus('start');

// Focus at a specific position
editor.commands.focus(42);

Skip Navigation ​

You can add a skip link before the editor for keyboard users:

html
<a href="#editor-content" class="sr-only focus:not-sr-only">
  Skip to editor
</a>

<!-- Other toolbar/navigation content -->

<div id="editor-content">
  <EdituEditor editor={editor} />
</div>

Focus Trap Considerations ​

Editu does not trap focus. You can Tab out of the editor to reach other page elements. If your application wraps the editor in a modal or dialog, implement focus trapping at the dialog level, not the editor level.


High Contrast Mode ​

CSS Custom Properties ​

Editu uses CSS custom properties for all colors, which makes it compatible with high contrast modes. You can override these variables to adjust contrast:

css
/* High contrast overrides */
@media (prefers-contrast: more) {
  :root {
    --editu-border: #000000;
    --editu-foreground: #000000;
    --editu-background: #ffffff;
    --editu-primary: #0000ff;
    --editu-ring: #0000ff;
    --editu-border-focus: #0000ff;
  }
}

Focus Indicators ​

Editu provides default focus styles, but you can enhance them to ensure visible focus indicators:

css
/* Enhanced focus indicators */
.editu-editor:focus-within {
  outline: 3px solid var(--editu-ring, #3b82f6);
  outline-offset: 2px;
}

/* Ensure bubble menu buttons show focus */
.editu-bubble-menu button:focus-visible {
  outline: 2px solid var(--editu-ring, #3b82f6);
  outline-offset: 1px;
}

Dark Mode ​

Editu supports dark mode through CSS custom properties. See Theming for configuration details.


Custom Component Guidelines ​

When you build custom components that interact with Editu, follow these accessibility guidelines.

Toolbar Buttons ​

tsx
<button
  type="button"
  role="button"
  aria-pressed={editor.isActive('bold')}
  aria-label="Bold"
  title="Bold (Ctrl+B)"
  onClick={() => editor.chain().focus().toggleBold().run()}
>
  <BoldIcon aria-hidden="true" />
</button>

Key attributes:

  • aria-pressed — Reflects the toggle state for formatting buttons
  • aria-label — Provides an accessible name when the button contains only an icon
  • title — Shows the keyboard shortcut on hover
tsx
<div role="listbox" aria-label="Text formatting options">
  {items.map((item, index) => (
    <div
      key={item.id}
      role="option"
      aria-selected={index === selectedIndex}
      tabIndex={index === selectedIndex ? 0 : -1}
    >
      {item.label}
    </div>
  ))}
</div>

Color Pickers ​

tsx
<div role="radiogroup" aria-label="Text color">
  {colors.map((color) => (
    <button
      key={color.value}
      role="radio"
      aria-checked={currentColor === color.value}
      aria-label={color.name}
      style={{ backgroundColor: color.value }}
    />
  ))}
</div>

Testing Accessibility ​

Manual Testing Checklist ​

  • [ ] Keyboard only — Navigate, edit, and format content using only the keyboard
  • [ ] Screen reader — Test with VoiceOver (macOS), NVDA (Windows), or Orca (Linux)
  • [ ] Zoom — Verify the editor is usable at 200% zoom
  • [ ] High contrast — Test with the OS high-contrast mode enabled
  • [ ] Reduced motion — Verify that animations respect prefers-reduced-motion
  • [ ] Color contrast — Confirm that text and interactive elements meet 4.5:1 contrast ratio

Reduced Motion ​

Respect the user's reduced motion preference:

css
@media (prefers-reduced-motion: reduce) {
  .editu-editor *,
  .editu-bubble-menu,
  .editu-slash-menu {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Automated Tools ​

You can use automated accessibility testing tools alongside manual testing:

  • axe-core — Automated accessibility testing engine
  • Lighthouse — Chrome DevTools accessibility audit
  • WAVE — Web accessibility evaluation tool
bash
# Example: Run axe-core in Playwright tests
npm install @axe-core/playwright

# In your test file
import AxeBuilder from '@axe-core/playwright';

test('editor has no accessibility violations', async ({ page }) => {
  await page.goto('/editor');
  const results = await new AxeBuilder({ page }).analyze();
  expect(results.violations).toEqual([]);
});

Next Steps ​

Released under the MIT License.