@codecademy/gamut

68.6.068.6.1-alpha.bc8b32.0
agent-tools/skills/gamut-accessibility/SKILL.md
+agent-tools/skills/gamut-accessibility/SKILL.mdNew file
+214
Index: package/agent-tools/skills/gamut-accessibility/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-accessibility/SKILL.md
+++ package/agent-tools/skills/gamut-accessibility/SKILL.md
@@ -0,0 +1,214 @@
+---
+name: gamut-accessibility
+description: Deep Gamut accessibility reference (component matrix, overlays, tips, live regions, checklists). Form wiring and validation UX live in **`gamut-forms`**. Universal HTML/ARIA/focus/color rules: always-loaded **`accessibility.mdc`** — read that first; this skill does not duplicate them.
+---
+
+# Gamut Accessibility
+
+Source: `@codecademy/gamut` — **`react-aria-components`** is used only in **[`packages/gamut/src/Tabs/`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Tabs/)** (`Tabs.tsx`, `TabList.tsx`, `Tab.tsx`, `TabPanel.tsx`). **`react-focus-on`** powers **`FocusTrap`** ([`packages/gamut/src/FocusTrap/index.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/FocusTrap/index.tsx)), used by overlays such as **`Overlay`** ([`packages/gamut/src/Overlay/index.tsx`](https://github.com/Codecademy/gamut/blob/main/packages/gamut/src/Overlay/index.tsx)) and **`Popover`**. Other widgets (e.g. **`Menu`**, **`DatePicker`**) implement keyboard and ARIA in Gamut code — verify behavior in Storybook and source, do not assume React Aria.
+
+**Product-oriented button variants and props:** [`guidelines/components/buttons.md`](../../guidelines/components/buttons.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 [`buttons.md`](../../guidelines/components/buttons.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). 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. [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)
+
+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                                                        |