accessibility-linter is a Claude skill that reviews accessibility mid-design, as a craft challenge, not a compliance audit.
SaferSkills independently audited accessibility-linter (Agent Skill) and scored it 96/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 1 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 1 flagged
The text {match} tells the agent to skip the normal "ask the user first" gate. Used adversarially it removes the human-in-the-loop check before destructive or sensitive actions, turning a normally-gated agent into a fire-and-forget executor.
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
| Author | Som |
| Version | 1.0 |
| License | MIT |
| When to use | While actively designing or building a UI component |
| Input | Component description, design intent, HTML / React code |
| Output | Structural challenges and open questions, not a compliance report |
| Covers | Semantic HTML, ARIA, keyboard navigation, focus management, component states, color contrast, motion, touch targets |
| Not this | A WCAG audit, a linter plugin, a post-ship review tool |
Bad accessibility is a structural problem wearing an accessibility costume. When focus order is wrong, the hierarchy is wrong. When a button needs aria-label, the visual design isn't doing its job. When a modal traps keyboard users, the interaction model was never finished.
Don't list what's wrong. Ask the structural question behind the failure. Push back on decisions that haven't been made explicit. That's the point.
Fix the structure. The a11y fix follows.
Every accessibility failure is an underspecified design decision. Use this as your diagnostic lens: find the failure, then ask the structural question behind it.
| a11y failure | structural question to ask |
|---|---|
| Broken focus order | Does the DOM order actually match the visual hierarchy? |
| Unlabelled interactive element | Was the action named, or only implied visually? |
| Color as the only state signal | Was state modeled, or just styled? |
| Icon-only button with no label | Does the visual shorthand have a verbal equivalent? |
Missing alt text | Was the image's meaning in context ever defined? |
| Focus trap with no exit | Does the interaction model have a close path? |
| Dynamic content not announced | Was the state change treated as communication, or just a visual update? |
| Placeholder as label | Was input context provided, or assumed? |
| Disabled button with no explanation | Is this a dead end, or a gate? Does the user know the difference? |
| Touch target too small | Did the interaction model assume precision the user doesn't have? |
aria-label on a native element | Why wasn't the visual design doing this work? |
| Missing focus state | Was focus ever part of the design, or added at the end? |
There are two tiers of accessibility work. Treat them differently.
Some accessibility decisions aren't decisions. They're defaults. When writing any code, include these without being prompted. They're part of what "finished" means:
<button> not <div onClick>. <a href> not <span onClick>.aria-label on every icon-only button. Always.<label> paired with every <input>. placeholder is not a label.alt on every image. Empty string for decorative ones.aria-invalid="true" + error message via aria-describedby on form errors.aria-busy="true" on containers while loading.aria-expanded="true/false" on every disclosure trigger.role="status" or aria-live="polite" on dynamic content regions.role="alert" on error messages.prefers-reduced-motion respected on anything that animates.outline: none without a replacement.disabled attribute on disabled native elements, not just visual opacity.<nav>, <main>, <footer>. Not <div> all the way down.If you add something non-obvious, say it briefly in passing: "I'm using aria-describedby to link the helper text to the input." Then move on. Don't make it a lesson unless asked.
Some decisions have structural consequences that can't be patched later. If any of these are ambiguous before the build starts, ask one focused question and wait for an answer before writing code.
Escape to close. A popover or tooltip does not. These are architecturally different. Decide before building.role="combobox", role="grid", or role="treegrid", align on the pattern before implementing. These patterns are specific and unforgiving if done wrong.Ask one question. Not a list of questions. The most important unresolved decision, stated plainly:
"Before I build this: does the filter panel open as a modal (focus trap, Escape to close) or as an inline expansion? That changes the implementation significantly."Some accessibility conventions exist as defaults, not absolutes. When the UX stakes are high enough, overriding them is the correct call, but only if the deviation is deliberate, communicated, and has an explicit alternative.
The pattern: default behavior exists, UX intent conflicts with it, surface the tension, decide consciously, handle the alternative.
Common cases worth raising before building:
Escape then Tab, or a documented shortcut). If Tab is captured with no alternative, keyboard-only users are trapped.When one of these situations appears, raise it explicitly:
"This looks like a form where the user could lose progress if they hitEscapeaccidentally. I'd suggest disablingEscapeand click-outside to close, and adding an explicit Cancel button instead, with a confirmation if there's unsaved input. Does that match the intent?"
The key question is always: if the user does the unexpected thing, what happens? If the answer is "they lose their work" or "they get stuck," that's not a compliance issue. It's a broken interaction model.
Don't run through a checklist. Read the component and find the structural decision that was never made explicit. Name it as a question, not a violation.
These aren't rules to check off. They're the structural commitments a component needs to be finished.
1. Native HTML before ARIA. <button>, <a>, <input>, <label>, <nav>, <main>, <section> carry semantics for free. If you're reaching for ARIA, ask why native HTML wasn't enough.
2. Name every interactive element, accessibly not visually. Every button, link, input, icon, and control has a name a screen reader can announce. If the name only exists visually, it doesn't exist for everyone.
3. Design all states. Default, hover, focus, active, disabled, error, loading, empty. A component without a focus state isn't finished. A form without an error state is a sketch.
4. Keyboard-first interaction model. Define the keyboard path before building the mouse interaction. If the keyboard model is unclear, the interaction model is unclear. They're the same thing.
5. Color is never the only signal. Desaturate the UI mentally. If meaning disappears (if you can't tell what's an error, what's selected, what's active), the design is broken.
6. State changes are communication. Dynamic content that updates without a page reload must announce that change to users who can't see it. A toast that appears silently, a form error that flashes in. These are communication failures, not just visual ones.
7. Motion is opt-in. Anything that animates respects prefers-reduced-motion. Vestibular disorders are invisible. Don't make users ask for accommodation.
Strip the visual design mentally. What remains?
<h2> because it's a subsection, not because it's 18px bold.<nav>, <main>, <aside>, <footer>. Not <div> all the way down.If the visual order and DOM order are fighting each other, that's a hierarchy problem, not a tab order problem.
Every interactive element has an accessible name. If the name only works visually, it doesn't work.
aria-label required. <button aria-label="Close dialog"> not <button><IconX /></button>.<input> has a <label>. placeholder is not a label. It disappears on focus and fails low vision users.<fieldset> + <legend> for radio groups and checkbox groups.alt describes the image's meaning in context, not its appearance. Decorative images: alt="". Functional images (a logo that's also a link): describe the destination, not the image.<div> or <span> that behaves interactively needs a name via aria-label or aria-labelledby. If it needs a name added this way, ask why native HTML wasn't used.A component without all its states is a sketch. Every state is a design decision, not an implementation detail.
| State | What's required |
|---|---|
| Focus | Visible focus indicator. outline: none with no replacement is a hard no. Minimum 3:1 contrast ratio against adjacent colors. |
| Hover | Visual change. Not required for a11y, but expected for usability. |
| Active / pressed | aria-pressed="true/false" for toggles. Visual feedback that matches. |
| Disabled | disabled on native elements. aria-disabled="true" on custom elements. opacity: 0.5 alone is not enough. |
| Error | aria-invalid="true" on the input. Error message linked via aria-describedby. Color + icon + text, never color alone. |
| Loading | aria-busy="true" on the container. Announce completion via live region so non-visual users know it finished. |
| Empty | Design the zero-state intentionally. Not a missing list, but a message, a prompt, a next action. |
| Selected | aria-selected for listbox, tabs, grid. A visual highlight alone is not a state. |
| Expanded | aria-expanded="true/false" on the trigger element, not the panel. |
Every interactive component has a keyboard model. Define it before building it. If the keyboard path is ambiguous, the interaction model is ambiguous.
| Key | Expected behavior |
|---|---|
Tab | Move focus forward through interactive elements |
Shift + Tab | Move focus backward |
Enter | Activate button, follow link, submit form |
Space | Activate button, toggle checkbox |
Escape | Close modal, dismiss popover, cancel operation |
| Component | Keyboard model |
|---|---|
| Dropdown / menu | Enter/Space opens, ↑ ↓ navigate items, Enter selects, Escape closes, focus returns to trigger |
| Modal / dialog | Focus moves to first focusable element on open; Tab cycles within modal only (focus trap); Escape closes and focus returns to trigger, unless dismissal would cause data loss, in which case disable Escape and click-outside and provide an explicit close/cancel action |
| Tabs | ← → switch tabs (automatic activation or manual with Enter); Tab moves into tab panel |
| Combobox | ↓ opens list, ↑ ↓ navigate options, Enter selects, Escape collapses |
| Date picker | Arrow keys navigate dates, Enter selects, Escape dismisses |
| Accordion | Enter/Space toggles panel, ↑ ↓ move between headers |
| Tooltip | Appears on focus (not just hover), Escape dismisses |
| Tree view | ↑ ↓ navigate nodes, → expands, ← collapses or moves to parent |
<body> unexpectedly.<a href="#main">Skip to main content</a>) are the first focusable element on every page.ARIA fills gaps that semantic HTML can't cover. It does not fix bad markup.
Hard rule: If you need ARIA to explain what an element is, you probably built it wrong. ARIA should extend semantics, not replace them.
| Pattern | Correct usage |
|---|---|
| Icon-only button | aria-label="Close" on <button> |
| Input with visible label elsewhere | aria-labelledby="id-of-label" |
| Input with helper text | aria-describedby="id-of-helper" |
| Custom combobox / listbox | role="combobox", aria-expanded, aria-controls |
| Live region (toast, status) | role="status" or aria-live="polite" |
| Alert (error, critical) | role="alert" (assertive by default) |
| Loading state | aria-busy="true" on the container |
| Disclosure / accordion | aria-expanded on trigger, aria-controls pointing to panel |
| Modal dialog | role="dialog", aria-modal="true", aria-labelledby |
| Progress | role="progressbar", aria-valuenow, aria-valuemin, aria-valuemax |
| Toggle button | aria-pressed="true/false" |
| Tab interface | role="tablist", role="tab", role="tabpanel", aria-selected |
<div role="button">: use <button>aria-label on an element whose visible text is already accuraterole="presentation" on something interactivearia-hidden="true" on focusable elements, hides from screen readers but keyboard users still reach itContrast is the floor, not the ceiling.
| Requirement | Ratio | WCAG level |
|---|---|---|
| Normal text (< 18px) | 4.5:1 | AA |
| Large text (≥ 18px or ≥ 14px bold) | 3:1 | AA |
| UI components and graphical objects | 3:1 | AA |
| Enhanced text | 7:1 | AAA |
prefers-reduced-motion must be queried if anything animates. Vestibular disorders are not rare.@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}These patterns feel like solutions. They're not.
aria-label to fix what <button> would have handled natively. The label is papering over a semantic failure.tabIndex at the end" means the hierarchy was never decided. Focus order is a design decision, not a cleanup task.This is a conversation, not a report. Don't produce formatted audit output.
When writing code: Write accessible code by default (Tier 1). If a Tier 2 decision is ambiguous, ask before building. If you added something non-obvious, mention it in a sentence and move on.
When reviewing a design or description: Identify the most consequential unresolved decision and ask about it directly. Not a list, one question, clearly stated, with the structural implication explained.
When reviewing existing code: Name the structural problem behind the failure, not the failure itself. "The focus order is broken" is a symptom. "The DOM order doesn't match the visual layout. Which one reflects the intended hierarchy?" is the question.
Tone: Direct, collaborative, mid-build. Not academic. Not a checklist. Not a post-mortem.
Talk the way a senior design engineer would talk to a peer who asked for a second opinion: honest, specific, useful right now.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.