Accessibility
Purpose
Make interfaces usable without a mouse, without sight, and without perfect colour perception — and be able to prove it against WCAG 2.2 AA rather than assert it.
When to Use
- Auditing a page or component for accessibility.
- Building an interactive component: modal, menu, combobox, tabs.
- Before a release with a legal or contractual accessibility requirement.
- Fixing findings from an automated scan (which catch roughly a third of real issues).
Capabilities
- WCAG 2.2 AA audit against the actual success criteria.
- Semantic HTML and correct ARIA (including when to use none).
- Keyboard navigation, focus order, and focus trapping.
- Screen-reader testing and announcement design.
- Colour contrast and non-colour signalling.
Inputs
- The page, component, or flow.
- The conformance target (usually WCAG 2.2 AA).
- The assistive technologies in scope.
Outputs
- Findings mapped to specific WCAG success criteria, with severity.
- The fix for each, at the code level.
- A keyboard and screen-reader walkthrough of the corrected flow.
Workflow
- Use the keyboard first — Tab through the entire flow. Can you reach every control? Can you see where you are? Can you escape every trap? This single test finds most serious defects.
- Check the semantics — Headings in order, landmarks present, lists as lists, buttons as
<button>. A <div onclick> is invisible to a screen reader and unreachable by keyboard.
- Run the automated scan — axe or Lighthouse. It catches contrast, missing labels, and ARIA misuse. It will not catch a wrong reading order or a meaningless label.
- Test with a screen reader — VoiceOver on macOS, NVDA on Windows. Listen to the flow: does the announcement make sense without the visual context?
- Verify contrast — 4.5:1 for body text, 3:1 for large text and for UI component boundaries. Do not rely on the design tool's claim; measure the rendered result.
- Fix and re-verify — Each fix is re-tested by keyboard and screen reader, not just by the scanner.
Best Practices
- The first rule of ARIA is not to use ARIA. A native
<button>, <select>, or <dialog> is accessible for free; a re-implementation with role="button" is a maintenance liability that will drift.
- Never remove the focus outline without providing a visible replacement.
outline: none with nothing after it is a WCAG 2.4.7 failure.
aria-label overrides the visible text for screen-reader users. If they differ, a voice-control user saying the visible label cannot activate the control (WCAG 2.5.3).
- Colour alone must never carry meaning. A red border on an invalid field needs an icon or text as well.
- Focus must move into a modal when it opens, be trapped while it is open, and return to the trigger when it closes. Almost no hand-rolled modal does all three.
- Announce dynamic changes with a live region. A form that submits and updates silently leaves screen-reader users with no feedback.
Examples
Accessible modal: focus management and semantics:
function Modal({ open, onClose, title, children }: ModalProps) {
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (open) dialog.showModal(); // native focus trap + inert background + Esc handling
else dialog.close();
}, [open]);
return (
<dialog
ref={dialogRef}
aria-labelledby="modal-title"
=> { if (e.target === dialogRef.current) onClose(); }}
>
<h2 id="modal-title">{title}</h2>
{children}
<button
</dialog>
);
}
The native <dialog> element with showModal() provides the focus trap, background inertness, Esc to close, and focus restoration — all of which a <div role="dialog"> requires you to implement and maintain by hand.
Error state that does not rely on colour, and is announced:
<div>
<label for="email">Email address</label>
<input id="email" type="email" aria-invalid="true" aria-describedby="email-error" />
<p id="email-error" role="alert">
<svg aria-hidden="true" ...></svg>
Enter an email address in the format name@example.com
</p>
</div>
Notes
- Automated tools detect roughly 30-40% of WCAG issues. A clean axe report is a starting point, not a conformance claim.
- WCAG 2.2 added target size (2.5.8, 24×24 CSS pixels minimum) and focus appearance criteria. Interfaces that passed 2.1 may fail 2.2 on small icon buttons.
- Skip links must be the first focusable element and must become visible on focus. A skip link that stays hidden when focused helps nobody.
1---2name: accessibility3description: Use when auditing or building accessible interfaces. Covers WCAG 2.2 AA conformance, semantic HTML, keyboard navigation, screen-reader behavior, focus management, and contrast.4---56# Accessibility78## Purpose910Make interfaces usable without a mouse, without sight, and without perfect colour perception — and be able to prove it against WCAG 2.2 AA rather than assert it.1112## When to Use1314- Auditing a page or component for accessibility.15- Building an interactive component: modal, menu, combobox, tabs.16- Before a release with a legal or contractual accessibility requirement.17- Fixing findings from an automated scan (which catch roughly a third of real issues).1819## Capabilities2021- WCAG 2.2 AA audit against the actual success criteria.22- Semantic HTML and correct ARIA (including when to use none).23- Keyboard navigation, focus order, and focus trapping.24- Screen-reader testing and announcement design.25- Colour contrast and non-colour signalling.2627## Inputs2829- The page, component, or flow.30- The conformance target (usually WCAG 2.2 AA).31- The assistive technologies in scope.3233## Outputs3435- Findings mapped to specific WCAG success criteria, with severity.36- The fix for each, at the code level.37- A keyboard and screen-reader walkthrough of the corrected flow.3839## Workflow40411. **Use the keyboard first** — Tab through the entire flow. Can you reach every control? Can you see where you are? Can you escape every trap? This single test finds most serious defects.422. **Check the semantics** — Headings in order, landmarks present, lists as lists, buttons as `<button>`. A `<div onclick>` is invisible to a screen reader and unreachable by keyboard.433. **Run the automated scan** — axe or Lighthouse. It catches contrast, missing labels, and ARIA misuse. It will not catch a wrong reading order or a meaningless label.444. **Test with a screen reader** — VoiceOver on macOS, NVDA on Windows. Listen to the flow: does the announcement make sense without the visual context?455. **Verify contrast** — 4.5:1 for body text, 3:1 for large text and for UI component boundaries. Do not rely on the design tool's claim; measure the rendered result.466. **Fix and re-verify** — Each fix is re-tested by keyboard and screen reader, not just by the scanner.4748## Best Practices4950- The first rule of ARIA is not to use ARIA. A native `<button>`, `<select>`, or `<dialog>` is accessible for free; a re-implementation with `role="button"` is a maintenance liability that will drift.51- Never remove the focus outline without providing a visible replacement. `outline: none` with nothing after it is a WCAG 2.4.7 failure.52- `aria-label` overrides the visible text for screen-reader users. If they differ, a voice-control user saying the visible label cannot activate the control (WCAG 2.5.3).53- Colour alone must never carry meaning. A red border on an invalid field needs an icon or text as well.54- Focus must move into a modal when it opens, be trapped while it is open, and return to the trigger when it closes. Almost no hand-rolled modal does all three.55- Announce dynamic changes with a live region. A form that submits and updates silently leaves screen-reader users with no feedback.5657## Examples5859**Accessible modal: focus management and semantics:**6061```tsx62function Modal({ open, onClose, title, children }: ModalProps) {63 const dialogRef = useRef<HTMLDialogElement>(null);6465 useEffect(() => {66 const dialog = dialogRef.current;67 if (!dialog) return;68 if (open) dialog.showModal(); // native focus trap + inert background + Esc handling69 else dialog.close();70 }, [open]);7172 return (73 <dialog74 ref={dialogRef}75 aria-labelledby="modal-title"76 onClose={onClose}77 onClick={(e) => { if (e.target === dialogRef.current) onClose(); }}78 >79 <h2 id="modal-title">{title}</h2>80 {children}81 <button onClick={onClose}>Close</button>82 </dialog>83 );84}85```8687The native `<dialog>` element with `showModal()` provides the focus trap, background inertness, `Esc` to close, and focus restoration — all of which a `<div role="dialog">` requires you to implement and maintain by hand.8889**Error state that does not rely on colour, and is announced:**9091```html92<div>93 <label for="email">Email address</label>94 <input id="email" type="email" aria-invalid="true" aria-describedby="email-error" />95 <p id="email-error" role="alert">96 <svg aria-hidden="true" ...></svg>97 Enter an email address in the format name@example.com98 </p>99</div>100```101102## Notes103104- Automated tools detect roughly 30-40% of WCAG issues. A clean axe report is a starting point, not a conformance claim.105- WCAG 2.2 added target size (2.5.8, 24×24 CSS pixels minimum) and focus appearance criteria. Interfaces that passed 2.1 may fail 2.2 on small icon buttons.106- Skip links must be the first focusable element and must become visible on focus. A skip link that stays hidden when focused helps nobody.