Use this skill when creating, modifying, or reviewing any .tsx component in apps/web, even if the user doesn't mention "accessibility." Covers semantic HTML, aria labels, navigation landmarks, forms, dialogs, and keyboard navigation. Trigger on: adding buttons, links, toggles, icons, or any interactive element; building or editing forms; adding dialogs or modals; reviewing UI code. Includes inline verification patterns for scanning violations. Not for styling or layout changes that don't involve interactive elements.
Every UI component in apps/web must meet these standards. No partial compliance.
role="button" divs may exist in the codebase -- fix them when touching affected files. <TableHead> elements with role="button" for sortable columns are acceptable.<div role="button"> that contains a child <button> (e.g., a copy button inside a collapsible toggle), do not just swap the outer div to <button>. That creates invalid nested buttons. Instead, restructure into sibling elements: a toggle <button> and a separate action <button> side by side in a flex container.useId() with htmlFor/id. The shadcn <FormField> handles this automatically, but raw <Input> does not.AlertDialog for destructive confirmations (requires + ). Use for content/forms. Never build custom modal overlays.AlertDialogTitleAlertDialogDescriptionDialogaria-label are common in new code. Every icon-only button needs one, and it must include context: Delete API key ${keyName}, not just "Delete".Use native elements. Never recreate <button> behavior with <div role="button"> + keyboard handlers.
| Interaction | Element |
|---|---|
| Clickable action | <button> or <Button> from @/components/ui/button |
| Navigation link | <Link> (Next.js) or <a> |
| Navigation group | <nav> with descriptive aria-label |
| Item list | <ul>/<ol> + <li> |
| Section heading | <h1>-<h6> in order, never skip levels |
Every interactive element without visible text needs aria-label with both action AND target:
<Button size="icon" aria-label={`Delete API key ${keyName}`}>
<Trash2 className="h-4 w-4" />
</Button>
<Switch aria-label={`${enabled ? "Disable" : "Enable"} skill ${skillName}`} />
Prefer state-aware labels ("Copied!" vs "Copy"). Buttons with visible text skip aria-label.
Reference implementations: copy-button.tsx (state-aware), context-attachment-badge.tsx (contextual remove), thread-table-header.tsx (sort state) -- all in apps/web/components/.
Wrap navigation groups in <nav> with a unique aria-label per region on the page.
Use shadcn Form components from @/components/ui/form (FormField, FormItem, FormLabel, FormControl, FormMessage). They handle ID generation, label association, aria-describedby, and aria-invalid automatically.
For standalone inputs outside react-hook-form, pair useId() with htmlFor/id. Never use placeholder as label substitute.
tabIndex={0} or tabIndex={-1} (never positive values)<button> over manual Enter/Space handlersScan apps/web/components for common violations. For each check, grep for the pattern and fix any matches found.
role="button" on non-button elementsSearch for role="button" in .tsx files. Flag <div or <span elements with this attribute; they should be <button> or <Button> instead. <TableHead> elements with role="button" for sortable columns are acceptable.
Pattern: role="button"
<div onClick> patternsSearch for <div elements with onClick handlers. These should use <button> instead for proper keyboard support.
Pattern: <div[^>]*onClick
Search for tabIndex with values greater than 0. Only tabIndex={0} and tabIndex={-1} are allowed.
Pattern: tabIndex={[1-9]
Search for size="icon" in .tsx files. For each match, check surrounding lines (5-10 above and below) for aria-label on the same <Button> element or an sr-only span. Flag buttons that have neither.
Pattern: size="icon" without nearby aria-label
These cannot be detected by pattern matching:
<label> elements<nav> with unique aria-label