Tambo validating-accessibility
install
source · Clone the upstream repo
git clone https://github.com/tambo-ai/tambo
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/tambo-ai/tambo "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/validating-accessibility" ~/.claude/skills/tambo-ai-tambo-validating-accessibility && rm -rf "$T"
manifest:
.claude/skills/validating-accessibility/SKILL.mdsource content
Accessibility Checklist
Every UI component in
apps/web must meet these standards. No partial compliance.
Gotchas
divs may exist in the codebase -- fix them when touching affected files.role="button"
elements with<TableHead>
for sortable columns are acceptable.role="button"- Nested interactive elements -- when replacing a
that contains a child<div role="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.<button> - Standalone inputs outside react-hook-form need manual ID pairing -- use
withuseId()
/htmlFor
. The shadcnid
handles this automatically, but raw<FormField>
does not.<Input> - AlertDialog vs Dialog -- use
for destructive confirmations (requiresAlertDialog
+AlertDialogTitle
). UseAlertDialogDescription
for content/forms. Never build custom modal overlays.Dialog - Icon-only buttons without
are common in new code. Every icon-only button needs one, and it must include context:aria-label
, not just "Delete".Delete API key ${keyName}
Semantic HTML
Use native elements. Never recreate
<button> behavior with <div role="button"> + keyboard handlers.
| Interaction | Element |
|---|---|
| Clickable action | or from |
| Navigation link | (Next.js) or |
| Navigation group | with descriptive |
| Item list | / + |
| Section heading | - in order, never skip levels |
Aria Labels
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/.
Navigation Landmarks
Wrap navigation groups in
<nav> with a unique aria-label per region on the page.
Forms
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.
Keyboard Navigation
- Only
ortabIndex={0}
(never positive values)tabIndex={-1} - Never remove focus outlines
- Prefer
over manual Enter/Space handlers<button>
Verification
Scan
apps/web/components for common violations. For each check, grep for the pattern and fix any matches found.
Check 1: role="button"
on non-button elements
role="button"Search 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"
Check 2: <div onClick>
patterns
<div onClick>Search for
<div elements with onClick handlers. These should use <button> instead for proper keyboard support.
Pattern:
<div[^>]*onClick
Check 3: Positive tabIndex values
Search for
tabIndex with values greater than 0. Only tabIndex={0} and tabIndex={-1} are allowed.
Pattern:
tabIndex={[1-9]
Check 4: Icon buttons missing aria-label
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
Manual checks
These cannot be detected by pattern matching:
- Form inputs have associated
elements<label> - Navigation groups use
with unique<nav>aria-label - Dialogs use Radix-based components (AlertDialog or Dialog)
- Focus outlines intact