accessibility-checklist
Original:🇺🇸 English
Translated
Accessibility review checklist for React/Next.js components built on Radix UI / shadcn/ui. Covers component library misuse, form accessibility, accessible names, keyboard interaction, focus management, and dynamic content. Loaded by pr-review-frontend.
1installs
Sourceinkeep/agents
Added on
NPX Install
npx skill4agent add inkeep/agents accessibility-checklistTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Accessibility Review Checklist
How to Use This Checklist
- Review changed components against the relevant sections below
- Not every section applies to every component — form checks only apply to form components, modal checks only apply to modals, etc.
- This codebase uses Radix UI / shadcn/ui extensively. These libraries handle most a11y patterns (keyboard nav, focus management, ARIA) automatically. Your primary job is to catch misuse of the library, not absence of manual implementation.
- When unsure whether a component library handles a pattern, lower confidence rather than asserting
§1 Component Library Misuse (Radix / shadcn/ui)
This is the highest-signal section for this codebase. Radix handles a11y correctly when used correctly — bugs come from misuse.
-
Dialog/Sheet without title: Radixand
DialogrequireSheet/DialogTitlefor screen reader announcement. IfSheetTitleis omitted or visually hidden withoutDialogTitleonaria-label, screen readers announce an unlabeled dialog.DialogContent- Common violation: with no
<DialogContent>and no<DialogTitle>aria-label - Note: Using is a valid pattern for dialogs where a visible title doesn't fit the design
<VisuallyHidden><DialogTitle>...</DialogTitle></VisuallyHidden>
- Common violation:
-
AlertDialog without description:should include
AlertDialogContentfor screen readers to understand the confirmation context. If omitted, addAlertDialogDescriptionto explicitly opt out (otherwise Radix warns).aria-describedby={undefined} -
Select/Combobox without accessible trigger label: Radixneeds
Selecton the trigger when there's no visible label. Customaria-labelandgeneric-select.tsxwrappers should propagate labels.generic-combo-box.tsx- Common violation: inside a form field that has a visual label, but the label isn't associated via
<Select>or wrappinghtmlFor
- Common violation:
-
DropdownMenu items without accessible names: Icon-only menu items need text content or. Menu items that are just icons (e.g., copy, delete, edit) need text.
aria-label- Correct pattern: (icon + text)
<DropdownMenuItem><TrashIcon /> Delete</DropdownMenuItem> - Violation: (icon only, no text, no aria-label)
<DropdownMenuItem><TrashIcon /></DropdownMenuItem>
- Correct pattern:
-
Tooltip as only accessible name: Tooltip text is not reliably announced by all screen readers. If a control's only accessible name is in a tooltip, it needsas well.
aria-label- Common pattern to flag: — Button needs
<Tooltip><TooltipTrigger><Button><Icon /></Button></TooltipTrigger><TooltipContent>Delete</TooltipContent></Tooltip>aria-label="Delete"
- Common pattern to flag:
-
Overriding Radix's keyboard handling: If a component wraps a Radix primitive and addsthat calls
onKeyDownore.preventDefault(), it may break Radix's built-in keyboard navigation.e.stopPropagation()
§2 Forms & Labels
The codebase uses react-hook-form + Zod with shadcn/ui's component, which auto-associates labels via context. Issues arise when forms bypass this pattern.
FormFormItem-
Every form input must have an accessible name: Via,
<FormLabel>,<label htmlFor={id}>, oraria-label. Placeholder text alone is NOT a label.aria-labelledby- Common violation: Custom inputs outside /
<FormField>that don't get auto-association<FormItem> - Common violation: used standalone without any label
<Input placeholder="Enter name" />
- Common violation: Custom inputs outside
-
Error messages must be associated with their input: shadcn/ui'sauto-associates via
<FormMessage>when insidearia-describedby. Custom error rendering outside this pattern loses the association.<FormItem>- Flag: Error text rendered near an input but not using or manual
<FormMessage>aria-describedby
- Flag: Error text rendered near an input but not using
-
Required fields must be indicated programmatically: Useor native
aria-required="true", not just a visual asterisk. Therequiredcomponent doesn't add this automatically — it comes from the Zod schema validation at submit time, not at the HTML level.Form -
Grouped controls need group semantics: Radio groups and checkbox groups should use(Radix) or
<RadioGroup>/<fieldset>. Loose radio buttons or checkboxes without group context confuse screen readers.<legend>- Scope: Configuration pages, settings forms, multi-option selectors
§3 Accessible Names (Icons & Buttons)
With 48 shadcn/ui components and heavy icon usage (Lucide React), icon-only interactive elements are a primary risk area.
-
Icon-only buttons must have: Buttons containing only an icon (no visible text) need
aria-labeldescribing the action.aria-label- Common violation: without
<Button variant="ghost" size="icon"><TrashIcon /></Button>aria-label - Very common in: data tables (row actions), toolbars, card headers, dialog close buttons
- Note: shadcn/ui's close button already includes
Dialog— don't flag this<span className="sr-only">Close</span>
- Common violation:
-
Icon-only links need accessible names: Same as buttons —or
<a>with only an icon needs<Link>.aria-label -
text is a valid alternative to
sr-only:aria-labelis correct. Don't flag this pattern as missing a label.<Button><TrashIcon /><span className="sr-only">Delete item</span></Button> -
Decorative icons should be hidden: Icons that are purely decorative (next to visible text) should haveto avoid redundant announcements.
aria-hidden="true"- Correct:
<Button><PlusIcon aria-hidden="true" /> Add item</Button> - Also correct: Lucide icons may set by default — check before flagging
aria-hidden
- Correct:
§4 Semantic HTML & Regression Guard
The codebase currently has no anti-patterns. This section guards against regressions.
<div onClick>-
Interactive elements must use native interactive HTML:for actions,
<button>/<a>for navigation. NOT<Link>,<div>, or<span>with<p>.onClick- Flag any new or
<div onClick>in the diff as CRITICAL<span onClick> - Exception: Components from Radix that render proper elements under the hood are fine
- Flag any new
-
Tables must use semantic HTML:,
<table>,<thead>,<tbody>,<th>. The codebase already does this. Flag any new data display that should be a table but uses<td>grid instead.<div>- Consider: elements should have
<th>orscope="col"for complex tablesscope="row"
- Consider:
-
Don't disable zoom: Flagor
user-scalable=noin viewport meta tags.maximum-scale=1
§5 Focus Management
Radix Dialog handles focus trap and restore automatically. This section covers what Radix doesn't handle.
-
Custom modals/overlays must manage focus: Any modal-like UI NOT built on Radix Dialog (e.g., custom overlays, fullscreen panels, React Flow side panels) must:
- Move focus into the overlay when it opens
- Trap focus while open (Tab cycles within the overlay)
- Return focus to the trigger when closed
- Close on Escape
-
Focus visible indicator must not be removed:/
outline-nonewithout aoutline: nonereplacement removes the only visual cue for keyboard users.focus-visible:ring-*- Note: The codebase consistently uses alongside
focus-visible:ring-*— this is correct. Only flag if a new component usesoutline-nonewithout the replacement.outline-none
- Note: The codebase consistently uses
-
Route change focus (Next.js App Router): After client-side navigation, focus should move to the main content. Next.js App Router may handle this — only flag if a custom route change mechanism bypasses the framework's handling.
-
Positive tabIndex is an anti-pattern:and
tabIndex={0}are fine.tabIndex={-1}or higher overrides natural order and creates unpredictable navigation. Flag any positive tabIndex values.tabIndex={1}
§6 Dynamic Content & Live Regions
With 287 toast usages (Sonner) and chat streaming interfaces, announcements for screen readers matter.
-
Sonner toasts: Sonner useswith
role="status"by default. This is correct. Only flag if:aria-live="polite"- A custom toast/notification bypasses Sonner and doesn't use a live region
- An error toast should use (assertive) instead of
role="alert"(polite) for critical errorsrole="status"
-
Loading states should be communicated: Skeleton loaders and spinners should be accompanied by screen reader announcements. Options:
- on the loading container
aria-busy="true" - inside the spinner
<span className="sr-only">Loading...</span> - region that announces "Loading..." then announces when content is ready
aria-live="polite" - Note: The codebase's component already has
Spinner— check that new loading patterns follow suitaria-label
-
Chat streaming messages: For the copilot/playground chat interfaces, new messages should be announced to screen readers. Thelibrary should handle this — only flag if custom chat rendering bypasses the library's announcements.
@inkeep/agents-ui -
Inline form validation: When validation errors appear dynamically (without page reload), they should either:
- Be associated with the input via (shadcn/ui's
aria-describedbydoes this)<FormMessage> - Or use to announce the error
aria-live="polite" - Only flag custom validation rendering outside the pattern
<FormMessage>
- Be associated with the input via
§7 Specialized Components
These components have unique a11y considerations beyond standard patterns.
-
Monaco Editor: Has known a11y limitations for screen reader users. When Monaco is used for required input (not just optional code editing), consider providing an alternative text input fallback. Flag only if a new Monaco instance is introduced without consideration.
-
React Flow (node graph editor): Keyboard navigation in visual node editors is inherently difficult. When React Flow is used:
- Ensure all node operations are also accessible via context menus or keyboard shortcuts
- Node labels should be readable by screen readers
- Flag only if new React Flow interactions are added without keyboard alternatives
-
Data tables with actions: Tables with row-level action buttons (common in this codebase) should ensure action buttons have accessible names and the table structure allows screen reader navigation.
- Flag: New table action buttons that are icon-only without
aria-label
- Flag: New table action buttons that are icon-only without
Severity Calibration
| Finding | Severity | Rationale |
|---|---|---|
| CRITICAL | Completely blocks keyboard/screen reader users |
| Keyboard trap (user cannot Tab out of a component) | CRITICAL | Completely blocks keyboard users |
| Custom modal without focus management (not using Radix Dialog) | MAJOR | Major disorientation for keyboard/screen reader users |
| Form input without accessible name (no label, no aria-label) | MAJOR | Screen reader users cannot identify the input |
Icon-only button without | MAJOR | Screen reader users cannot identify the action |
| Dialog without DialogTitle and no aria-label | MAJOR | Screen reader users don't know what the dialog is for |
| MAJOR | Creates ghost focus for screen reader users |
| Error message not associated with input (outside FormMessage) | MAJOR | Screen reader users don't know about validation errors |
| MAJOR | Keyboard users lose their place |
| Radix keyboard handling overridden via stopPropagation | MAJOR | Breaks built-in a11y of the component library |
| Missing alt text on informational image | MINOR | Information not conveyed, but usually not blocking |
Decorative icon missing | MINOR | Redundant announcement — annoying, not blocking |
| Custom notification/toast without live region | MINOR | Status not announced, but visually evident |
| Redundant ARIA on native elements | MINOR | Noise, not breakage — indicates misunderstanding |
Missing | INFO | Navigation degraded in complex tables, not blocking |