# Accessibility Patterns — Copy-Paste Reference ## Contents - Skip link - Focus styles - Accessible form field with error - Icon-only button - Live regions (async updates) - Accessible modal dialog - Reduced motion - Contrast tokens - Gotchas ## Skip link First focusable element on the page; visible only on focus. ```html
``` ```css .skip-link { position: absolute; left: -9999px; } .skip-link:focus { left: 8px; top: 8px; z-index: 100; } ``` ## Focus styles `:focus-visible` shows the ring for keyboard users without flashing it on every mouse click. ```css :focus-visible { outline: 2px solid var(--color-focus, #1a56db); outline-offset: 2px; } /* never do: :focus { outline: none; } without a replacement */ ``` ## Accessible form field with error ```html

Enter an email address with an @, like name@example.com.

``` Remove `aria-invalid` and the error node (or empty it) once the field validates. ## Icon-only button ```html ``` `aria-hidden` on the SVG keeps screen readers from announcing the icon twice. ## Live regions (async updates) ```html
``` ```js document.getElementById('status').textContent = '12 results found'; ``` The live region must exist in the DOM before you write into it — injecting a new `aria-live` node with text is not announced reliably. ## Accessible modal dialog Native `` gives focus trapping and Esc for free — prefer it. ```html

Delete file?

``` ```js const dlg = document.getElementById('confirm'); dlg.showModal(); // traps focus, Esc closes dlg.addEventListener('close', () => opener.focus()); // return focus to the trigger ``` ## Reduced motion ```css @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } ``` ## Contrast tokens Bake compliance into tokens so components can't ship a failing pair. ```css :root { --text-on-light: #1f2937; /* 14.7:1 on #ffffff */ --text-muted: #4b5563; /* 7.6:1 on #ffffff — still AA for body text */ --color-focus: #1a56db; /* 3.6:1 vs #ffffff — passes 3:1 UI minimum */ } ``` Check ratios with a tool (e.g., `npx wcag-contrast 4b5563 ffffff`) rather than by eye. ## Gotchas - `display: none` and `visibility: hidden` hide content from screen readers too; use a `.visually-hidden` clip-pattern class for screen-reader-only text. - `tabindex` values >0 break natural focus order — only ever use `0` and `-1`. - `role="button"` on a div does NOT add Enter/Space handling; you must write the keydown handler yourself (another reason to use `