@codecademy/gamut
68.2.268.2.3-alpha.b8b211.0
agent-tools/rules/accessibility.mdc+
agent-tools/rules/accessibility.mdcNew file+44
Index: package/agent-tools/rules/accessibility.mdc
===================================================================
--- package/agent-tools/rules/accessibility.mdc
+++ package/agent-tools/rules/accessibility.mdc
@@ -0,0 +1,44 @@
+---
+description: Accessibility guardrails for Gamut-based UI. Enforces semantic HTML, accessible names, ARIA discipline, and contrast.
+alwaysApply: true
+globs: ["*.tsx", "*.ts", "*.jsx", "*.js"]
+---
+
+# Gamut Accessibility Rules
+
+## No ARIA is better than bad ARIA
+
+Incorrect ARIA actively harms AT users — it is worse than no ARIA at all. Broken semantics, missing keyboard behavior, and misused roles mislead screen reader users about what a control is and how to operate it.
+
+**A Role is a Promise.** Adding `role="button"` to a `<div>` promises that you have also implemented every behavior a native button provides: focusability, Space/Enter activation, and correct event semantics. ARIA roles modify the accessibility tree — they do not add behavior. A `<div role="button">` that only handles `onClick` is broken for keyboard users because `click` events on non-interactive elements are not triggered by Space or Enter.
+
+Gamut components backed by React Aria keep this promise. Reach for them before reaching for ARIA attributes.
+
+## Use Gamut components — don't reimplement what they already solve
+
+- `<Button>` not `<div onClick>` or `<span role="button">`
+- `<Tabs>` / `<Tab>` / `<TabList>` / `<TabPanel>` — arrow key, Home, End navigation is automatic
+- `<Dialog>` / `<Modal>` — `FocusTrap` handles focus lock and `aria-modal`; Escape is wired
+
+## Every interactive control needs an accessible name
+
+- **`<IconButton>`** — provide `tip`; it becomes the accessible label (no separate `aria-label` needed)
+- **`<InfoTip>`** — provide `ariaLabel` or `ariaLabelledby`; there is no automatic fallback
+- **`<Dialog>` / `<Modal>`** — always set `aria-label` or `aria-labelledby`
+- Icon SVGs alongside visible text — add `aria-hidden="true"` to the icon element
+
+## Form label association
+
+Always match `htmlFor` on `<FormGroupLabel>` with the `id` on the input. `<FormGroup>` auto-wires `aria-live` for its `error` and `description` props — do not add redundant live regions manually.
+
+## Screen-reader-only text
+
+Use `<Text screenreader>` for visually hidden but announced content. `<HiddenText>` is deprecated.
+
+## Color and contrast
+
+Do not hardcode hex values. Gamut's semantic color tokens through `ColorMode` are validated for WCAG AA contrast. Hardcoded values bypass that guarantee and break in dark mode.
+
+## Focus visibility
+
+Never suppress focus indicators with `outline: none` or `outline: 0` without a visible replacement. Gamut's focus styles are intentional and meet WCAG 2.4.7.