Browse all prompts
Make an interactive component accessible
Take a component that works with a mouse and make it work with a keyboard, a screen reader and reduced motion — real focus management and semantics, not a scattering of ARIA attributes.
Make this component genuinely accessible: **[component name and file path]** Adding ARIA attributes until the linter goes quiet is not the job. Most of the fix is usually semantics and focus, and most bad ARIA is worse than none. ## 1. Semantics before attributes Check what it is built from. A `div` with an `onclick` becomes a `button`. A list of things becomes a `ul`. A disclosure becomes a `button` carrying `aria-expanded="true|false"` for its state, and `aria-controls="<id>"` to point at the region it opens — two different attributes doing two different jobs. Every ARIA attribute you keep, justify to me. If a native element does the same job, use the native element and delete the attribute — the browser already handles the states, the keyboard and the announcements, and your version will not. ## 2. Keyboard Work through it with the mouse unplugged: - Everything interactive is reachable by <kbd>Tab</kbd>, in the order it appears on screen — reachable, which for a composite widget means the widget takes one tab stop and the arrow keys move within it, not a stop per item. - Each control answers the keys its native equivalent answers: a button on <kbd>Enter</kbd> and <kbd>Space</kbd>, a link on <kbd>Enter</kbd> alone. Do not bind <kbd>Space</kbd> on a link — it scrolls the page, and readers expect that. - <kbd>Escape</kbd> closes anything that overlays, and focus returns to whatever opened it. - Composite widgets — tabs, menus, listboxes — use arrow keys with one tab stop, not a tab stop per item. - No keyboard trap, except a modal dialog, where the trap is the point. - Visible focus on every stop. `:focus-visible`, never `outline: none` with nothing to replace it. ## 3. Screen reader - Every control has an accessible name that says what it does. A `title` can act as a fallback name, but do not rely on it: it draws a tooltip that needs a pointer, so it reaches neither a keyboard nor a touch screen. Put the name in the markup — a visible label, or visually hidden text when the visible label is an icon — and keep `title` for at most a hover convenience. - State changes that matter are announced, once. For a result count or a save confirmation, put `role="status"` on a small dedicated region that holds just that sentence. Wrapping a whole list in `aria-live` is the blunter instrument: what gets read depends on `aria-atomic` and on how the list mutates, so it is easy to end up re-announcing far more than changed. - Icons that carry no information get `aria-hidden="true"`. ## 4. The rest - **Contrast**: 4.5:1 for body text and 3:1 for large text, where large means at least 24px regular or about 18.7px bold. A visual cue that is the only thing identifying a control needs 3:1; purely decorative edges do not. Check the hover and focus states too, which is usually where it fails. Disabled controls are exempt under WCAG — check them anyway and tell me what you find, but report it as a judgement call, not a failure. - **Reduced motion**: honour `prefers-reduced-motion: reduce`, for scripted animation as well as CSS transitions. For a small transition, shortening it is fine; for anything that moves a long way or scales, remove the movement or swap it for a fade, because a fast large movement still triggers what the setting exists to prevent. Always leave the element in its final state. - **Unique ids per instance.** Two of these on one page must not collide. In Svelte 5 use `$props.id()` rather than a module-level counter. - **Cleanup.** Every listener and timer removed when the component is destroyed. - **SSR safety.** No `window` or `document` at module scope or during render. ## 5. Verify Tab through it and tell me the focus order you observed. Run an automated check (axe or similar) and report what it says — but do not stop at a clean automated result, because it cannot tell you whether the focus order makes sense or whether the accessible name is accurate. Finish with: what you changed, what ARIA you deleted and why, the contrast numbers for each state, and anything still wrong that needs a design decision from me.