make-ui-accessible
Load this skill when writing or reviewing component markup with interactive elements, images, forms, or navigation in any web frontend. Auto-load when a .svelte or .tsx file's HTML structure is writte
SKILL.mdplugins/standalone/ceh-web-frontend/skills/make-ui-accessible/SKILL.md
GitHubMake the UI Accessible
Write markup that works from the keyboard and a screen reader without extra effort. Done when every interactive element is reachable, labelled, and passes the a11y lint for the framework.
Procedure
- Start from the native HTML element for the job, and add ARIA only where none fits.
- Check that every action is keyboard-reachable with a visible focus indicator.
- Label every form control and every image.
- Fix all a11y lint warnings before opening a PR.
Rules
Baseline
- All interactive elements must be keyboard-accessible
- Images must have
altattributes (emptyalt=""for decorative images) - Use semantic HTML (
<button>,<nav>,<main>,<section>,<header>) — not<div>for everything - Fix all a11y lint warnings before opening a PR:
svelte-check(SvelteKit) oreslint-plugin-jsx-a11y(React) - Only the attribute and event syntax differs by framework:
onclick/bind:value/forin Svelte,onClick/value+onChange/htmlForin React. The ARIA attributes are identical.
Keyboard navigation
- Every action reachable by mouse must be reachable by keyboard
- Visible focus indicator must never be removed (
outline: nonewithout a replacement is forbidden) - Tab order must follow the visual reading order
- Modal dialogs must trap focus while open and return focus to the trigger on close
ARIA
Use native HTML semantics first. Add ARIA only when no native element fits:
<button>Submit</button>
<button aria-label="Close dialog">✕</button>
<p aria-live="polite" aria-atomic="true">Saved</p>
<button aria-expanded="false" aria-controls="details">Details</button>
Bind dynamic values with the framework's expression syntax: aria-expanded={open} in both Svelte and JSX.
Do not add role="button" to a <div> — use <button>. Never use aria-hidden="true" on a focusable element.
Forms
<label for="topic">Topic</label>
<input id="topic" type="text" />
<input id="email" aria-describedby="email-error" aria-invalid="true" />
<span id="email-error" role="alert">Enter a valid email address</span>
In JSX the label attribute is htmlFor. Set aria-invalid from the validation state, and render the error element only while it is set.
Color and contrast
- Text contrast ratio: 4.5:1 minimum (3:1 for large text ≥ 18px or bold ≥ 14px)
- Do not rely on color alone to convey information — pair with an icon or text label