agentsclimarketplace

Accessibility linter

Skill designedbysom/accessibility-linter

accessibility-linter is a Claude skill that reviews accessibility mid-design, as a craft challenge, not a compliance audit.

Install
npx -y skills add designedbysom/accessibility-linter

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Reviews accessibility mid-design, not as a compliance audit, but as a craft challenge. Use when designing, describing, or building a UI component. Trigger on "I'm designing a component", "does this work", "review this interaction", "how should I handle focus", "is this accessible", or whenever HTML/CSS/React code is shared. Also trigger when component states, interaction models, or keyboard behavior are being described. Don't wait to be asked. If a component is being actively designed, this skill is relevant.

SKILL.md

23.3 KB, as published. Nobody here has run it

accessibility-linter

AuthorSom
Version1.0
LicenseMIT
When to useWhile actively designing or building a UI component
InputComponent description, design intent, HTML / React code
OutputStructural challenges and open questions, not a compliance report
CoversSemantic HTML, ARIA, keyboard navigation, focus management, component states, color contrast, motion, touch targets
Not thisA 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.


What every a11y failure is really saying

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 failurestructural question to ask
Broken focus orderDoes the DOM order actually match the visual hierarchy?
Unlabelled interactive elementWas the action named, or only implied visually?
Color as the only state signalWas state modeled, or just styled?
Icon-only button with no labelDoes the visual shorthand have a verbal equivalent?
Missing alt textWas the image's meaning in context ever defined?
Focus trap with no exitDoes the interaction model have a close path?
Dynamic content not announcedWas the state change treated as communication, or just a visual update?
Placeholder as labelWas input context provided, or assumed?
Disabled button with no explanationIs this a dead end, or a gate? Does the user know the difference?
Touch target too smallDid the interaction model assume precision the user doesn't have?
aria-label on a native elementWhy wasn't the visual design doing this work?
Missing focus stateWas focus ever part of the design, or added at the end?

How this skill thinks

There are two tiers of accessibility work. Treat them differently.


Tier 1: Build it in, don't ask

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.
  • Visible focus styles. Never outline: none without a replacement.
  • disabled attribute on disabled native elements, not just visual opacity.
  • Landmark elements: <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.


Tier 2: Ask before building

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.

  • Custom keyboard navigation: tabs, combobox, menu, tree view, date picker. These have defined patterns. Confirm which pattern before building the interaction.
  • Focus management: when something opens, where does focus go? When it closes, where does it return? If this isn't clear, ask.
  • Modal vs. non-modal: does this overlay trap focus or not? A modal traps focus and needs Escape to close. A popover or tooltip does not. These are architecturally different. Decide before building.
  • Live region strategy: if content updates frequently (real-time data, polling), does every update need to be announced? Constant announcements are as bad as none. Decide the cadence.
  • Complex ARIA patterns: if the component requires role="combobox", role="grid", or role="treegrid", align on the pattern before implementing. These patterns are specific and unforgiving if done wrong.
  • Intentional deviations from default patterns: see below.

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."


Intentional deviations: when breaking the default is right

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:

  • Disabling Escape and click-outside on a modal: correct for multi-step flows, complex forms, or any modal where accidental dismissal causes data loss. The modal must have an explicit close or cancel action, and if there's unsaved state, the user needs to be told before they leave.

  • Capturing Tab in an editor or custom input: a code editor or rich text field that uses Tab for indentation is correct to do so, but keyboard users need an escape hatch (Escape then Tab, or a documented shortcut). If Tab is captured with no alternative, keyboard-only users are trapped.

  • Blocking form submission until a selection is made: valid for comboboxes and typeaheads where free text isn't allowed. The constraint needs to be communicated before the user tries to submit, not only after they fail.

  • Preventing scroll or navigation during a flow: acceptable in multi-step wizards or onboarding sequences where losing context is genuinely disorienting. The user always needs a visible way out, even if it abandons progress.

  • Sticky or fixed elements that intercept focus: overlapping a focused element with a sticky header is a common, silent failure. If sticky elements are used, scroll-padding or JavaScript focus management needs to account for them.

When one of these situations appears, raise it explicitly:

"This looks like a form where the user could lose progress if they hit Escape accidentally. I'd suggest disabling Escape and 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.


When reviewing existing code or a design

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.

  • What's missing is usually more telling than what's wrong.
  • One open decision often explains multiple surface failures.
  • If something is genuinely solid, say so and move on.

Core principles

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.


Structure and hierarchy

Strip the visual design mentally. What remains?

  • What is the logical reading order without CSS? Does it match what the visual design implies?
  • Does the DOM order match the visual order? If they differ, there's a reason. State it explicitly.
  • Are headings used for document hierarchy, not visual style? <h2> because it's a subsection, not because it's 18px bold.
  • Does the landmark structure map to how a user would describe the page? <nav>, <main>, <aside>, <footer>. Not <div> all the way down.
  • Is anything positioned visually in a way that breaks tab sequence?

If the visual order and DOM order are fighting each other, that's a hierarchy problem, not a tab order problem.


Naming and labels

Every interactive element has an accessible name. If the name only works visually, it doesn't work.

  • Buttons with text: The text is the name. Is it descriptive? "Delete account" beats "Delete". "Send message" beats "Submit".
  • Icon-only buttons: aria-label required. <button aria-label="Close dialog"> not <button><IconX /></button>.
  • Links: Describe the destination. "Read the case study" not "Read more". Two "Read more" links on the same page are two identical names for different destinations.
  • Inputs: Every <input> has a <label>. placeholder is not a label. It disappears on focus and fails low vision users.
  • Input groups: <fieldset> + <legend> for radio groups and checkbox groups.
  • Images: 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.
  • Custom components: Anything built from <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.

States

A component without all its states is a sketch. Every state is a design decision, not an implementation detail.

StateWhat's required
FocusVisible focus indicator. outline: none with no replacement is a hard no. Minimum 3:1 contrast ratio against adjacent colors.
HoverVisual change. Not required for a11y, but expected for usability.
Active / pressedaria-pressed="true/false" for toggles. Visual feedback that matches.
Disableddisabled on native elements. aria-disabled="true" on custom elements. opacity: 0.5 alone is not enough.
Erroraria-invalid="true" on the input. Error message linked via aria-describedby. Color + icon + text, never color alone.
Loadingaria-busy="true" on the container. Announce completion via live region so non-visual users know it finished.
EmptyDesign the zero-state intentionally. Not a missing list, but a message, a prompt, a next action.
Selectedaria-selected for listbox, tabs, grid. A visual highlight alone is not a state.
Expandedaria-expanded="true/false" on the trigger element, not the panel.

Keyboard navigation

Every interactive component has a keyboard model. Define it before building it. If the keyboard path is ambiguous, the interaction model is ambiguous.

Universal keys

KeyExpected behavior
TabMove focus forward through interactive elements
Shift + TabMove focus backward
EnterActivate button, follow link, submit form
SpaceActivate button, toggle checkbox
EscapeClose modal, dismiss popover, cancel operation

Component-specific patterns

ComponentKeyboard model
Dropdown / menuEnter/Space opens, ↑ ↓ navigate items, Enter selects, Escape closes, focus returns to trigger
Modal / dialogFocus 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 pickerArrow keys navigate dates, Enter selects, Escape dismisses
AccordionEnter/Space toggles panel, ↑ ↓ move between headers
TooltipAppears on focus (not just hover), Escape dismisses
Tree view↑ ↓ navigate nodes, expands, collapses or moves to parent

Focus management rules

  • Focus moves into a component when it opens (modal, dialog, dropdown).
  • Focus returns to the trigger when a component closes.
  • Focus is never lost. It never lands on <body> unexpectedly.
  • Modals trap focus. Nothing else does.
  • Skip links (<a href="#main">Skip to main content</a>) are the first focusable element on every page.

ARIA: use it correctly, not liberally

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.

When to use ARIA

PatternCorrect usage
Icon-only buttonaria-label="Close" on <button>
Input with visible label elsewherearia-labelledby="id-of-label"
Input with helper textaria-describedby="id-of-helper"
Custom combobox / listboxrole="combobox", aria-expanded, aria-controls
Live region (toast, status)role="status" or aria-live="polite"
Alert (error, critical)role="alert" (assertive by default)
Loading statearia-busy="true" on the container
Disclosure / accordionaria-expanded on trigger, aria-controls pointing to panel
Modal dialogrole="dialog", aria-modal="true", aria-labelledby
Progressrole="progressbar", aria-valuenow, aria-valuemin, aria-valuemax
Toggle buttonaria-pressed="true/false"
Tab interfacerole="tablist", role="tab", role="tabpanel", aria-selected

When not to use ARIA

  • <div role="button">: use <button>
  • aria-label on an element whose visible text is already accurate
  • role="presentation" on something interactive
  • Multiple roles stacked on one element
  • aria-hidden="true" on focusable elements, hides from screen readers but keyboard users still reach it

Color and contrast

Contrast is the floor, not the ceiling.

RequirementRatioWCAG level
Normal text (< 18px)4.5:1AA
Large text (≥ 18px or ≥ 14px bold)3:1AA
UI components and graphical objects3:1AA
Enhanced text7:1AAA
  • Every state (focus, hover, disabled, error) needs its own contrast check. A passing default state doesn't exempt the others.
  • Test with a colorblind simulator, not just a contrast checker. Contrast ratios don't catch hue-only distinctions.
  • If the only signal is color (red = error, green = success), add an icon or text label. Color alone is not a state.

Motion

  • prefers-reduced-motion must be queried if anything animates. Vestibular disorders are not rare.
  • Reduced motion is not no motion. Avoid large translations, scaling, and spinning. Fades and opacity changes are usually fine.
  • Nothing autoplays for more than 5 seconds without a pause control.
  • Parallax scrolling is a vestibular hazard. Opt-in only.
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Touch

  • Minimum touch target: 44×44px (Apple HIG, WCAG 2.5.5)
  • Minimum spacing between targets: 8px to prevent mis-taps
  • No hover-only interactions. Every tooltip, dropdown, or reveal that works on hover must also work on tap and keyboard.
  • Every swipe gesture has a button equivalent. Swipe is an enhancement, not the only path.

Anti-patterns

These patterns feel like solutions. They're not.

  • ARIA as a patch. Using aria-label to fix what <button> would have handled natively. The label is papering over a semantic failure.
  • Focus as an afterthought. "We'll add tabIndex at the end" means the hierarchy was never decided. Focus order is a design decision, not a cleanup task.
  • Happy path only. No focus state, no error state, no loading state. That's an unfinished component being called done.
  • Hover-only interactions. Tooltips, popovers, and dropdowns that only appear on hover exclude keyboard and touch users entirely.
  • outline: none with nothing. The most common focus failure in production. Always replace the focus indicator, never just remove it.
  • Identical link text. Three "Read more" links are three unlabeled destinations. The destination is part of the name.
  • aria-hidden on focusable elements. Hidden from screen readers, reachable by keyboard. Broken by design.
  • Designing for the prototype. Placeholder content, missing states, implied labels. These are fine in Figma. They're not fine in code.

How to talk

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.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.