# Hakal Team Skills Accessible Component Patterns

> Accessible Component Patterns

- Skill: `tomevault-io/hakal-team-skills-accessible-component-patterns` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add tomevault-io/hakal-team-skills-accessible-component-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tomevault-io/hakal-team-skills-accessible-component-patterns/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: tomevault-io (https://skillmd.com/u/tomevault-io)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tomevault-io/hakal-team-skills-accessible-component-patterns

---


# Accessible Component Patterns

## Component Patterns

### Button
- Use `<button>`, never `<div onclick>` or `<a>` for actions
- Toggle buttons: `aria-pressed="true|false"`
- Icon-only buttons: `aria-label="descriptive text"`
- Loading state: `aria-disabled="true"` + `aria-busy="true"`, keep label readable
- Keyboard: `Enter` and `Space` activate

### Link vs Button
- **Link**: navigates somewhere (`<a href>`)
- **Button**: does something (`<button>`)
- Never use `<a>` without `href`. Never use `<a href="#" onclick>` for actions.

### Modal Dialog
- `role="dialog"` + `aria-modal="true"` + `aria-labelledby="title-id"`
- **Focus trap**: Tab cycles within modal, never escapes to background
- On open: move focus to first focusable element (or the close button)
- On close: return focus to the element that triggered the modal
- `Escape` closes the modal
- Background content: `aria-hidden="true"` on content behind modal, or `inert` attribute

### Dropdown Menu
- Trigger: `aria-haspopup="true"` + `aria-expanded="true|false"`
- Menu: `role="menu"`, items: `role="menuitem"`
- Keyboard: `Enter`/`Space` opens, `ArrowDown`/`ArrowUp` navigates, `Escape` closes
- On open: focus moves to first menu item
- On close: focus returns to trigger

### Tabs
- Container: `role="tablist"`
- Tab: `role="tab"` + `aria-selected="true|false"` + `aria-controls="panel-id"`
- Panel: `role="tabpanel"` + `aria-labelledby="tab-id"`
- Keyboard: `ArrowLeft`/`ArrowRight` between tabs, `Tab` moves into panel content
- Active tab: `tabindex="0"`, inactive tabs: `tabindex="-1"`

### Combobox (Autocomplete)
- Input: `role="combobox"` + `aria-expanded` + `aria-controls="listbox-id"` + `aria-autocomplete="list|both"`
- Options: `role="listbox"` > `role="option"`
- Active option: `aria-activedescendant="option-id"` on the input
- Keyboard: `ArrowDown`/`ArrowUp` navigate options, `Enter` selects, `Escape` closes list
- Announce result count: live region with "X results available"

### Toast / Notification
- Use `role="status"` (polite) for info, `role="alert"` (assertive) for errors
- Add `aria-live="polite"` or `aria-live="assertive"`
- Don't auto-dismiss error toasts — user may need time to read
- Info toasts: auto-dismiss is OK (5+ seconds), provide pause on hover
- Don't stack too many — screen readers announce each one

### Form
- Every input needs a `<label>` with `for` attribute (or wrapping)
- Error messages: `aria-describedby` pointing to error text + `aria-invalid="true"`
- Required fields: `aria-required="true"` (or HTML `required`)
- Group related fields: `<fieldset>` + `<legend>`
- Submit feedback: announce success/failure via live region, not just visual change

### Accordion
- Trigger: `<button>` with `aria-expanded="true|false"` + `aria-controls="panel-id"`
- Panel: region with `role="region"` + `aria-labelledby="trigger-id"`
- Keyboard: `Enter`/`Space` toggles, standard focus order (no arrow key nav needed)

## Focus Management Patterns

### Focus Trap
Contain focus within a region (modals, drawers):
- Track first and last focusable elements
- On `Tab` from last: move to first
- On `Shift+Tab` from first: move to last
- Use `inert` attribute on background content when available

### Focus Restoration
When closing an overlay, return focus to the trigger:
- Store `document.activeElement` before opening
- Restore on close
- If trigger no longer exists, focus nearest logical ancestor

### Skip Link
First focusable element on the page, hidden until focused:
- "Skip to main content" linking to `<main>` or `#content`
- Visible on `:focus`, hidden otherwise
- Must be the first item in tab order

### Roving Tabindex
For widget-internal navigation (tabs, toolbars, menus):
- Active item: `tabindex="0"`
- All other items: `tabindex="-1"`
- Arrow keys move focus and update tabindex values
- `Tab` exits the widget entirely

## Screen Reader Patterns

### Live Regions
- `aria-live="polite"`: announced after current speech finishes (status updates, search results count)
- `aria-live="assertive"`: interrupts current speech (errors, urgent alerts)
- Add the live region to DOM first, then update its content — screen readers only announce *changes*

### Visually Hidden Content
For screen-reader-only text:
```css
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
```
Never use `display: none` or `visibility: hidden` for SR-only content — those hide from screen readers too.

### Dynamic Content
- Loading states: `aria-busy="true"` on the container, announce "Loading" via live region
- Infinite scroll: announce new content count, maintain focus position
- Single-page navigation: announce new page title via live region, move focus to `<h1>` or `<main>`

## Testing Checklist

For every interactive component:
- [ ] Keyboard-only operation (no mouse required)
- [ ] Visible focus indicator on every focusable element
- [ ] Screen reader announces purpose, state, and changes
- [ ] Color contrast 4.5:1 for text, 3:1 for UI elements
- [ ] Touch target minimum 44x44px on mobile
- [ ] No content conveyed by color alone
- [ ] Works with 200% zoom without horizontal scroll

---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/hakal) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-11 -->

