Use this skill when implementing accessibility for a specific Gamut component, building a custom overlay or composite widget, or auditing component usage against WCAG — complements the always-loaded `accessibility.mdc` with Gamut component-specific patterns. Form wiring lives in `gamut-forms`; Modal/Dialog composition detail lives in `gamut-modal`.
Scanned 9/1/2026
Install to Claude Code
npx -y skills add Codecademy/gamut --skill gamut-accessibility --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Gamut Accessibility?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/codecademy-gamut-accessibility)More formats (shields.io, HTML) on the badges page.
---
name: gamut-accessibility
description: Use this skill when implementing accessibility for a specific Gamut component, building a custom overlay or composite widget, or auditing component usage against WCAG — complements the always-loaded `accessibility.mdc` with Gamut component-specific patterns. Form wiring lives in `gamut-forms`; Modal/Dialog composition detail lives in `gamut-modal`.
---
# Gamut Accessibility
Source: `@codecademy/gamut` — `react-aria-components` is used only in `packages/gamut/src/Tabs/` (`Tabs.tsx`, `TabList.tsx`, `Tab.tsx`, `TabPanel.tsx`). `react-focus-on` powers `FocusTrap` (`packages/gamut/src/FocusTrap/index.tsx`), used by overlays such as `Overlay` (`packages/gamut/src/Overlay/index.tsx`) and `Popover`. Other widgets (e.g. `Menu`, `DatePicker`) implement keyboard and ARIA in Gamut code.
Product-oriented button variants and props: [`gamut-buttons`](../gamut-buttons/SKILL.md)
---
## Universal rules
Prefer native HTML, minimal ARIA, correct roles, visible names, focus visibility, semantic color / `ColorMode`, and Gamut primitives — see the always-loaded Gamut Accessibility Rules: [`accessibility.mdc`](../../rules/accessibility.mdc). This skill adds Gamut component behavior and audit detail below.
---
## How Gamut handles accessibility
Tabs use `react-aria-components` (see `packages/gamut/src/Tabs/*.tsx`) for roving tabindex and keyboard navigation. Overlays (e.g. `Overlay`, `Popover`) use `FocusTrap` → `react-focus-on` for focus containment and Escape/outside close. Other interactive components (`Menu`, `DatePicker`, `Modal`, `Dialog`, etc.) rely on in-repo implementations — supply accessible names, wire labels to controls, and avoid duplicating what a component already sets (`aria-live`, `aria-describedby`, tabindex, etc.); confirm in source when auditing.
---
## Component reference (index)
There is no exported `<Button>` — use `FillButton`, `TextButton`, `StrokeButton`, `CTAButton`, and `IconButton` (shared `ButtonProps` type). Prefer these over `<div onClick>` or `<span role="button">`.
Forms — `FormGroup`, `ConnectedForm` / `ConnectedFormGroup`, `GridForm`, field atoms (`Select`, `Checkbox`, `Radio`), validation, `aria-live` / `aria-describedby`: canonical reference is [`gamut-forms`](../gamut-forms/SKILL.md).
| Component(s) | Handled in library | App / author responsibilities |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `FillButton`, `TextButton`, `StrokeButton`, `CTAButton` | Render `<button>` (or `<a>` when `href` is set); native click + keyboard activation | Visible text or `href` purpose; follow [`gamut-buttons`](../gamut-buttons/SKILL.md) for variants and `disabled` vs `aria-disabled`. |
| `IconButton` | `tip` feeds the accessible name for icon-only controls | Always pass `tip` when the button has no visible text. |
| `Dialog` | `Overlay` (shroud, Escape, focus), `role="dialog"`, `aria-modal`, close control with configurable `closeButtonProps.tip` | Provide a clear `title` (and meaningful body copy). Full props/composition: [`gamut-modal`](../gamut-modal/SKILL.md). Confirm naming with [Molecules / Modals / Dialog](https://gamut.codecademy.com/?path=/docs-molecules-modals-dialog--docs). |
| `Modal` | Same overlay/focus stack; optional `aria-label`; multi-`views` support | `title` / view titles; pass `aria-label` when there is no visible title string. Full props/composition: [`gamut-modal`](../gamut-modal/SKILL.md). [Molecules / Modals / Modal](https://gamut.codecademy.com/?path=/docs-molecules-modals-modal--docs). |
| `Alert` | Default `aria-live="polite"`, `role="status"` | Use `aria-live="assertive"` only for urgent interruptions; do not nest inside another live region. |
| `Tabs`, `Tab`, `TabList`, `TabPanel` | `react-aria-components` in `packages/gamut/src/Tabs/` — roving tabindex, arrows, Home/End | Name each tab; Tab moves into the active panel per APG. |
| Forms | See Forms above | [`gamut-forms`](../gamut-forms/SKILL.md) |
| `DatePicker` + `DatePickerInput` | Segmented input + calendar behavior inside `FormGroup` | Provide `label` / `name` / `form` as for any input; keep `DatePickerInput` inside `DatePicker`. When embedded in `FormGroup` / `GridForm`, follow [`gamut-forms`](../gamut-forms/SKILL.md). [Organisms / DatePicker](https://gamut.codecademy.com/?path=/docs-organisms-datepicker--docs). |
| `Menu`, `MenuItem`, `MenuSeparator` | List + `MenuProvider` (keyboard / roles depend on variant) | Label `Menu` / menubar per pattern; follow Storybook [Molecules / Menu](https://gamut.codecademy.com/?path=/docs-molecules-menu--docs). |
| `Popover` | `FocusTrap` when open (unless `skipFocusTrap`), positioning | `onRequestClose`, meaningful `role` when needed; do not trap focus unnecessarily when `skipFocusTrap`. |
| `Flyout` | `Overlay`, `Drawer`, visible `title`, close `IconButton` with `tip={closeLabel}` | Pass `title` and `closeLabel`; name panel content. |
| `Drawer` | Focuses container when `expanded`, `tabIndex={-1}` on shell | Drawer is a surface, not a full dialog — supply headings/labels inside for screen readers. |
| `Disclosure` | `DisclosureButton` drives expand/collapse | Provide `heading` / structure so the control’s purpose is clear. |
| `Toggle` | `ToggleLabel` + `htmlFor` wired to control `id` | With no visible `label`, pass `ariaLabel` (or use `as="button"` pattern per props). |
| `ToolTip` | Floating mode renders a screen-reader `role="tooltip"` branch with `id` | Pass the same `id` to the trigger’s `aria-describedby` when you rely on the tooltip as supplementary description (see component `id` JSDoc). |
| `InfoTip` | — | `ariaLabel` or `ariaLabelledby` (camelCase) — no automatic fallback. |
| `PreviewTip` | `Anchor`-based preview, focus-driven content | `linkDescription` and visible anchor text; do not use the preview as the sole name for an unrelated control. |
| `SkipToContent` | Skip link behavior | Place early in the tab order; `href` target `id` must exist on main content. |
| `Toast` + `Toaster` | `Toaster` wraps the stack in `aria-live="polite"` | Keep messages concise; avoid stacking many simultaneous assertive announcements. |
| `Pagination` | Page / control buttons | Ensure current page and actions are perceivable (labels / `aria-current` patterns per Storybook). [Molecules / Pagination](https://gamut.codecademy.com/?path=/docs-molecules-pagination--docs). |
| `FocusTrap` | Escape, outside click, `allowPageInteraction` | Return focus to trigger on close for custom overlays. |
```tsx
// correct
<IconButton icon={DeleteIcon} tip="Delete item" onClick={handleDelete} />
// wrong — no accessible name, no keyboard semantics
<div onClick={handleDelete}><DeleteIcon /></div>
```
### Dialog / Modal (detail)
Full composition, props, and the focus-trap/autofocus/dismiss mechanics in detail: [`gamut-modal`](../gamut-modal/SKILL.md). Summary:
Both use `Overlay` and `FocusTrap` (`react-focus-on`) patterns: focus moves into the surface, Escape closes (when enabled), focus should return to the trigger on close.
Prefer a visible title so the dialog has a clear name; on `Modal`, pass `aria-label` when there is no suitable visible title string. Close control: `IconButton` with `closeButtonProps.tip` (defaults documented in source).
```tsx
<Dialog
title="Confirm deletion"
confirmCta={{ children: 'Delete', onClick: handleDelete }}
onRequestClose={handleClose}
isOpen={open}
/>
```
### Alert (detail)
Renders with `aria-live="polite"` and `role="status"` by default. Override with `aria-live="assertive"` only for time-sensitive errors requiring immediate interruption. Do not nest `<Alert>` inside another live region.
### Tabs (detail)
Built on `react-aria-components`. Follows the [ARIA Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/): arrow keys navigate tabs, Tab moves focus into the active panel, Home/End jump to first/last tab. The tablist is a composite — only the active tab is in the tab sequence (roving tabindex). No manual `aria-selected` or keyboard handling needed.
### InfoTip (example)
`<InfoTip>` needs `ariaLabel` or `ariaLabelledby` — see also the always-loaded rules.
```tsx
<InfoTip ariaLabel="More information about billing" />
```
### ToolTip (detail)
When `placement="floating"`, the component renders a screen-reader-only branch with `role="tooltip"` and an optional `id`. Pass the same `id` to the described element’s `aria-describedby` so assistive tech associates the tooltip copy with the trigger. Inline placement uses the wrapper differently — see [Molecules / Tips / ToolTip](https://gamut.codecademy.com/?path=/docs-molecules-tips-tooltip--docs).
### SkipToContent (detail)
Include `<SkipToContent>` as the first focusable element in the page shell. The main content region must expose a matching `id` for the skip target.
---
## Focus management
`<FocusTrap>` is for custom overlay patterns not covered by `Dialog` / `Modal`.
Key props:
- `active` — enable/disable the trap dynamically
- `onEscapeKey` — close handler
- `onClickOutside` — dismiss on outside click
- `allowPageInteraction` — permit scrolling outside the trap without closing
Always return focus to the trigger on close. `react-focus-on` (via `FocusTrap`) and overlay flows handle much of this for dialogs/popovers; `Tabs` inherit focus behavior from `react-aria-components`. Custom surfaces must store a ref to the trigger and call `.focus()` on close when the library does not.
---
## Composite widgets and managed focus
ARIA composite roles (`listbox`, `menu`, `tree`, `grid`, `tablist`) use managed focus: only one element in the composite is in the tab sequence at a time. Tab moves focus to the next element outside the composite; arrow keys move focus within it.
Implementation pattern — roving tabindex:
- Set `tabIndex={0}` on the currently active item
- Set `tabIndex={-1}` on all other items
- On arrow key, update which item holds `tabIndex={0}` and call `.focus()` on it
`Tabs` (`react-aria-components`) implement roving tabindex for the tablist pattern. `Menu` and other composites implement focus in Gamut — if you build a custom composite, implement roving tabindex yourself. A flat `tabIndex={0}` on every item is wrong — it puts every item in the sequential tab order.
---
## Device-independent events
Use `click` for activation, not `mousedown`. `click` follows pointer activation; native `<button>` (and similar controls) also fire `click` from keyboard (Space and Enter). A focused `<a href>` is usually activated with Enter, which fires `click` — Space often scrolls the page instead of activating the link. `mousedown` does not represent keyboard activation, so relying on it alone breaks keyboard-only use.
For custom elements with `role="button"`, do not assume the browser will synthesize `click` from the keyboard the way it does for native interactive elements (`<button>`, `<a href>`, and other built-ins). Handle `keydown` for Space and Enter explicitly:
```tsx
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
handleActivation();
}
};
```
Prefer Gamut `*Button` components (or `Anchor` with a real `href`) so you do not reimplement this.
---
## Live regions
| Scenario | Pattern |
| ------------------------------------------ | ------------------------------------------- |
| Status updates, non-critical notifications | `aria-live="polite"` |
| Urgent global interruptions | `aria-live="assertive"` (use sparingly) |
| Frequently updating counts or progress | `aria-live="polite"` + `aria-atomic="true"` |
Form-bound `aria-live` and `FormError` patterns: see Forms above (do not assume assertive on every field error).
Inject live regions into the DOM before they need to announce. A region added simultaneously with its first announcement may be ignored by some assistive technologies.
Do not elevate unrelated inline errors to `assertive` — reserve assertive for urgent interruptions the user did not directly trigger.
---
## ARIA authoring rules
- No redundant roles: don't set `role="button"` on `<button>` or `role="heading"` on `<h2>`
- `aria-hidden` cascades: placing `aria-hidden="true"` on a parent removes the entire subtree from the accessibility tree, including focusable descendants — never put it on an ancestor of a focusable element
- `role="presentation"` and `aria-hidden` on focusable elements: both are prohibited on elements that can receive focus — they remove semantics while leaving the element keyboard-reachable, producing an operable but unnamed control
- Labelling vs describing: `aria-label` / `aria-labelledby` name the control. `aria-describedby` provides supplementary context. Both can coexist on the same element
- Required fields: use `aria-required="true"` or the HTML `required` attribute. Visual asterisks must have an explanatory text string visible on the page; the asterisk glyph itself should carry `aria-hidden="true"` — `<FormGroupLabel>` already handles this
- `display:none` vs `aria-hidden`: elements with `display:none` are already removed from the accessibility tree; adding `aria-hidden` is redundant. Use `aria-hidden="true"` only when an element is visually present but should be hidden from assistive technology
---
## Color and contrast (non-text)
Semantic tokens, `ColorMode`, and `<Background>` are covered in the always-loaded `accessibility.mdc` rule and the `gamut-color-mode` skill. Here: non-text contrast (focus rings, input borders, icon affordances) should meet ~3:1 vs adjacent colors where WCAG 1.4.11 applies — validate in your layout.
---
## Testing checklist
- [ ] Full keyboard navigation: every interactive element reachable and operable without a mouse
- [ ] Focus is always visible and never lost or unexpectedly trapped
- [ ] Dialogs trap focus correctly; Escape closes; focus returns to the trigger
- [ ] Composite widgets (tabs, menus, listboxes) use arrow keys internally, not Tab
- [ ] All form inputs have programmatically associated labels (not placeholder-only)
- [ ] Form errors surface through the library’s `FormError` / live-region patterns (Forms above)
- [ ] Icon-only controls have accessible names
- [ ] No content relies solely on color to convey meaning
- [ ] Screen reader matrix: VoiceOver + Safari (iOS), VoiceOver + Chrome (macOS), NVDA + Chrome (Windows)
- [ ] 200% zoom: layout intact, no content overflow or disappearance
---
## Common anti-patterns
| Anti-pattern | Fix |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `<div onClick={…}>` for actions | `FillButton`, `TextButton`, `StrokeButton`, `CTAButton`, or `IconButton` (with `tip`) |
| `placeholder` as the only label | `FormGroupLabel` with matching `htmlFor` / `id` |
| `aria-label` on a `<div>` with no role | Add a meaningful `role` or use a semantic element |
| `role="button"` without Space/Enter handlers | Use a Gamut `*Button`, `Anchor` with `href`, or add `keydown` |
| `tabIndex={0}` on every item in a composite | Roving tabindex: `0` on active item, `-1` on rest |
| Tooltip as the only accessible name for a control | Set `aria-label` (or visible text) on the control as well |
| `aria-hidden="true"` on a focusable element | Also remove from tab order (`tabIndex={-1}`) or restructure |
| `mousedown` for activation | Use `click` |
| `outline: none` without a replacement | Use Gamut’s built-in focus styles |
| Multiple `aria-live` regions for the same content stream | One region per logical stream; reuse it across updates |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!