Generated banner for Better Accessibility
Generated logo for Better Accessibility

Better Accessibility

Jakub KrehelCommunityCreated by Jakub Krehel

  • Skill
  • Review and audit
  • Software development
  • MIT
  • No issues found

From the author

Reviews and fixes keyboard and focus behavior, ARIA, accessible names, forms, screen-reader announcements, motion and zoom against WCAG 2.2.
— the description by Jakub Krehel

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured methodology for auditing and fixing web accessibility issues. It focuses on keyboard navigation, screen-reader compatibility, ARIA usage, form behavior, and motion sensitivity. The skill emphasizes using native HTML elements over ARIA, ensuring visible focus indicators, and maintaining proper semantic structures. It guides the user through manual testing procedures and provides a reporting format to document findings, severity levels, and specific code-level remediations based on WCAG 2.2 criteria.

What it does

  • Audits keyboard and focus behavior to ensure full navigation support.
  • Validates ARIA usage and accessible names for screen-reader compatibility.
  • Checks form controls for proper labeling, validation, and error announcement.
  • Verifies hit area sizes and pointer interaction alternatives.
  • Ensures motion and zoom support, including reduced motion preferences.
  • Generates structured reports mapping findings to WCAG 2.2 criteria.

When to use it

  • When performing a manual accessibility audit of a web interface.
  • When debugging issues with keyboard navigation or focus management.
  • When verifying that custom UI components meet accessibility requirements.
  • When preparing a report on accessibility failures and their corresponding fixes.

Advantages

  • Prioritizes native HTML elements to reduce reliance on complex ARIA.
  • Provides clear, criterion-based reporting for actionable remediation.
  • Includes specific guidance for handling focus traps and dynamic content.
  • Encourages consistent testing patterns for keyboard and screen-reader users.

Usage examples

“Audit my login form for accessibility issues.”
The agent will review the form's labels, input types, error handling, and keyboard navigation, reporting any failures against WCAG 2.2 criteria.
“Check if my custom modal implementation is accessible.”
The agent will verify focus trapping, Escape key support, and proper ARIA roles, providing code fixes if the implementation deviates from standard patterns.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/jakubkrehel/better-accessibility/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip better-accessibility.zip -d .agents/skills

Documentation by Jakub Krehel

Accessibility

This skill reviews and fixes semantics, keyboard and focus behavior, accessible names, forms, announcements, motion and zoom. It reports each failure against the WCAG criterion it breaks and writes the fix in the project's styling system.

Reviewing means two walks. Keyboard-only, where every flow completes without a mouse. Then screen-reader, where every control announces a name, a role and its state. When unsure, take the platform default over a custom rebuild, and remove ARIA rather than add it.

Contrast measurement and color fixes belong to better-colors. Text sizing, iOS input zoom and language metadata belong to better-typography. Spacing between targets and spatial RTL belong to better-layout. Label, error and alt-text wording belong to better-writing. Animation recipes, hover styling and scroll containment belong to better-ui.

Criteria, not conventions

A finding cites a WCAG 2.2 Level A or AA criterion by number, or a concrete task an assistive-technology user cannot complete. Everything else is a recommendation and never HIGH. That covers AAA criteria, APG conventions, the one-<h1> and no-skipped-levels outline and the 44px and 40px targets. The criterion thresholds below are exact. The larger targets are heuristics, so keep an established, usable density.

Native elements first

The first rule of ARIA: don't use ARIA when a native element exists. <button> for actions, <a href> for navigation, never <div onClick>. A real link must support Cmd/Ctrl/middle-click. See semantics-and-aria.md for landmarks and button-vs-link.

Disabled means unavailable

Use native disabled when a control is genuinely unavailable. Reach for aria-disabled="true" only when it should stay focusable, then block pointer, keyboard and form behavior in code and style the state explicitly. The rules are in semantics-and-aria.md.

Visible focus rings

Style :focus-visible, not bare :focus. Keyboard users get a ring and mouse users usually don't. Prefer the browser's unmodified indicator.

A custom ring needs a project focus token or another explicit color. It must reach 3:1 against every adjacent color it crosses, currentColor included, under 1.4.11. A 2px solid perimeter with a 3:1 change of contrast is the 2.4.13 AAA target. Never use outline: none without a verified replacement, and preserve system colors in forced-colors mode.

A focused element must never sit fully hidden behind a sticky header, footer or banner (2.4.11). Pad the scroller by the sticky chrome's height, a fix better-layout owns. Recipes are in focus-and-keyboard.md.

Full keyboard support

Every pointer interaction needs a keyboard path. Follow the ARIA APG patterns: Escape closes overlays, arrow keys move within composite widgets, Tab moves between widgets, Enter and Space activate buttons.

Use only tabindex="0" to join the natural tab order and tabindex="-1" for programmatic focus. Positive values break that order. Composite widgets use roving tabindex, where the active item is 0 and every other is -1. Where focus must stay in an input, as in a combobox, use aria-activedescendant instead. Key maps per widget are in focus-and-keyboard.md.

Trap and restore focus

Prefer <dialog> opened with showModal(), which makes the background inert and handles Escape. A custom overlay sets inert on the background, role="dialog" and aria-modal="true" on itself. Either way, move focus inside on open and return it to the trigger on close.

Client-side route changes reset nothing. Update document.title and move focus to the new view's <h1> or <main>. Both recipes are in focus-and-keyboard.md.

Minimum hit area

WCAG 2.5.8's Level AA baseline is a 24×24 CSS-pixel target, or one of its exceptions. Aim for 44×44px on touch and 40×40px on desktop where density permits. Extend with a pseudo-element when the visible element should stay smaller.

Never let extended hit areas overlap. Give decorative layers pointer-events: none, so a glow never swallows the clicks meant for the control beneath it.

Every drag interaction needs a single-pointer alternative, such as buttons or a menu that reorder or move the same item (2.5.7). Sizes, exceptions and collision rules are in hit-areas.md.

Label and type every control

Every input gets a <label for> or a wrapping <label>. A placeholder is never a label. Label and control share one hit target, with no dead zone between a checkbox and its text.

Add a valid autocomplete token and a descriptive name attribute, plus the type and inputmode that summon the right keyboard. Never block paste; users paste passwords and one-time codes. See forms.md.

Errors that announce

Validate on submit, never by disabling submit until the form is valid. Mark failing fields aria-invalid="true", point aria-describedby at the inline error text and focus the first invalid field. Submit behavior while the request runs is in forms.md.

Accessible names everywhere

Icon-only buttons need a descriptive aria-label. Visible label text must appear in the accessible name. Decorative elements get aria-hidden="true", never on a focusable element.

Pair color with a second cue

Status needs a redundant cue: an icon, text or an underline alongside the color (1.4.1).

This skill decides which contrast requirement applies:

CriterionApplies toMinimum
1.4.3 AAText4.5:1
1.4.3 AALarge text, at least 24px or 18.67px bold3:1
1.4.11 AAUI component boundaries, states and meaningful graphics3:1

Inactive controls and logos are exempt. Use better-colors to measure the rendered pair. When it fails, report the pair and the criterion it misses, and leave the colors alone unless asked.

Honor prefers-reduced-motion

Wrap motion in @media (prefers-reduced-motion: no-preference) so it is opt-in. Under reduced motion, replace slides and scales with opacity cross-fades, and kill parallax and autoplay entirely. See motion-and-zoom.md.

Nothing the user needs runs on a timer

Anything moving, blinking or updating on its own for more than 5 seconds needs a visible pause control (2.2.2). Toasts carrying an action or an error stay until dismissed. The rules are in motion-and-zoom.md.

Announce dynamic content

Three mechanisms, three jobs. aria-describedby carries field-specific validation. A polite live region (role="status") carries non-urgent updates not tied to a control, such as toasts and result counts. role="alert" carries urgent untied errors and nothing else.

Repeated polite announcements need a stable empty region rendered before its text updates. Dynamically inserted alerts vary in support, so test them on the screen readers you target. See screen-readers.md.

Alt text by purpose

Decorative images get alt="". Informative images describe the meaning. Functional images describe the action, never the picture. The full table is in screen-readers.md.

Structure is navigation

Use headings that describe their sections and form a coherent outline. Expose one visible primary <main> landmark. When repeated navigation or chrome precedes it, make a "Skip to content" link the first focusable element. Anchored headings get scroll-margin-block-start.

Survive zoom and text resize

Text must survive 200% resize (1.4.4), and the page must reflow at 320px width without horizontal scrolling (1.4.10). Text containers take min-height, not height, and better-layout owns the rest of that fix. Never set maximum-scale=1 or user-scalable=no in the viewport meta. iOS input zoom is fixed with better-typography's 16px rule instead.

Before you finish

PatternFix
outline: none or outline-none with no focus-visible replacement nearbyRestore the browser ring or add a verified custom one
<div onClick or <span onClickUse <button>, or <a href> when it navigates
role="button" or role="tab" with no onKeyDownUse the native element, or implement the APG key map
tabIndex={1} or any positive tabindexFix the DOM order and use 0
disabled={!isValid} on a submit buttonKeep it enabled and validate on submit
{msg && <div role="status">, region mounted with its textRender the empty region first and update its text
aria-live="assertive" on a success toastrole="status"
<img with no alt attributealt="" if decorative, otherwise describe the purpose
aria-label whose text omits the visible labelStart the name with the visible text
aria-hidden on an ancestor of a button, link or inputRemove it, or make the subtree inert
maximum-scale=1 or user-scalable=noRemove it
Tooltip on a natively disabled controlText beside it, or aria-disabled so it stays focusable
onDragStart or a drag library with no button alternativeAdd a single-pointer path to the same result

Reporting

Severity. HIGH prevents a task, hides content from assistive technology or creates a systemic failure. MEDIUM makes an interaction meaningfully harder. LOW is isolated polish. This domain's share of better-interface's escalation triggers is HIGH on sight. That is a missing accessible name, a missing focus indicator and a pointer path with no keyboard path. It is also motion ignoring reduced motion, loss at 320px or 200% and meaning carried by color alone.

Verification. Without a browser: accessible names on every interactive element, keyboard handlers on non-native controls, focus styles, prefers-reduced-motion guards and form labels bound to their inputs. With one: tab the flow in order, read computed names and roles from the accessibility tree, confirm a visible focus indicator at every stop and run an automated audit. Report every check you could not run as Not verified.

Format. Group findings under the principle each violates, ordered by severity, one row per root cause listing every location it appears in:

SeverityLocationBeforeAfterWhy

Location is path/to/file:line. Why names the WCAG criterion or principle and the user impact.

End with Block when any HIGH remains, Approve otherwise, leaving the rest in the table as work to do. Never Approve coverage you did not inspect. With nothing to report, state "No actionable accessibility findings" and report verification.

Files

semantics-and-aria.md

Semantics and ARIA

Native elements first, landmarks, accessible names and the ARIA rules that keep custom widgets honest.

The rules of ARIA

  1. If a native HTML element with the semantics and behavior you need exists, use it instead of repurposing another element with ARIA.

  2. Don't change native semantics unless you really have to.

  3. Every interactive ARIA control must be keyboard-operable; a role is a promise of the full keyboard model, states and behavior.

  4. All interactive elements must have an accessible name.

No ARIA is better than bad ARIA. A screen reader trusts your roles, so a wrong one is worse than none.

ElementUse forWhy
<a href>Navigation: anything that goes somewhere or changes the URLFree Cmd/Ctrl/middle-click, right-click → copy link, Enter activation
<button>Actions: submit, toggle, open, deleteFree focus, Enter and Space activation, form semantics
<div onClick>NothingNo role, no focus, no keyboard; screen readers see plain text
// Bad: invisible to keyboard and screen readers
<div onClick={openSettings}>Settings</div>

// Good: focus, Enter/Space activation and semantics for free
<button onClick={openSettings}>Settings</button>

A "button" that navigates is a styled <a>, and a "link" that performs an action is a styled <button>.

Where a native element is truly impossible, the full polyfill is role="button" plus tabindex="0" plus Enter and Space handlers, which is why the native element is always less code.

Landmarks and headings

  • <header>, <nav>, <aside> and <footer> map to landmarks screen-reader users jump between.

  • Multiple landmarks of the same type need distinguishing labels: <nav aria-label="Primary">, <nav aria-label="Breadcrumbs">.

  • Headings are structure, not styling. Style a heading level with CSS instead of picking the tag by size.

  • <title> matches the current context, most specific first: Billing · Settings · Acme (2.4.2).

Accessible names

Name precedence: aria-labelledby > aria-label > native label (<label>, text content, alt) > title attribute.

  • Prefer visible text or aria-labelledby over aria-label, which is invisible, drifts out of sync with the UI and is handled inconsistently by translation tools.

  • Icon-only buttons always need a name: <button aria-label="Close"> with the icon aria-hidden="true".

  • WCAG 2.5.3 Label in Name: a button showing "Send" with aria-label="Submit message" breaks voice control users who say "click Send".

// Good: name from visible text, icon hidden
<button>
  <TrashIcon aria-hidden="true" /> Delete
</button>

// Good: icon-only, explicit name
<button aria-label="Delete">
  <TrashIcon aria-hidden="true" />
</button>

Keeping brand names and code tokens out of auto-translation with translate="no" belongs to better-typography.

Common ARIA mistakes

MistakeWhy it fails
aria-label on a plain <div> or <span>ARIA 1.2 prohibits naming the generic role, and most screen readers ignore it
<button role="button">Redundant role; adds noise, no benefit
aria-labelledby/aria-describedby pointing at a missing IDSilently produces no name or description
role="menu" on a nav listmenu promises app-style arrow-key behavior; site navigation is <nav> with a list

Disabled states

Native disabled supplies the platform's complete disabled behavior. It removes the control from the tab order, suppresses activation, applies :disabled and excludes form controls from submission. aria-disabled="true" only announces the state, changing neither focusability, nor behavior, nor styling.

  • Never disable submit to gate validation. Validate on submit instead (forms.md).

  • A natively disabled control suppresses pointer events and leaves the tab order, so a tooltip on it never opens for keyboard or touch users. Put the reason in visible text beside the control, or switch to aria-disabled="true", which keeps it focusable and hoverable.

  • Use aria-disabled="true" where keeping a control discoverable in the tab order is intentional, or where a custom control cannot use native disabled.

  • With aria-disabled="true", block pointer and keyboard activation in the handler and prevent form submission where applicable. Add explicit styling, including forced-colors support, and explain nearby why the action is unavailable.

  • Never set both disabled and aria-disabled on the same element.

  • Disabled controls are exempt from contrast minimums.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Security

No issues found by our static scan· No issues found

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
Jakub Krehel
Publisher
Jakub Krehel (Community)
Source
jakubkrehel/skills · skills/better-accessibility · commit f41f027
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Jakub Krehel or Jakub Krehel.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download better-accessibility.zip