@codecademy/gamut

72.3.072.3.1-alpha.f273fd.0
agent-tools/skills/gamut-forms/SKILL.md
~agent-tools/skills/gamut-forms/SKILL.mdModified
+51−1
Index: package/agent-tools/skills/gamut-forms/SKILL.md
===================================================================
--- package/agent-tools/skills/gamut-forms/SKILL.md
+++ package/agent-tools/skills/gamut-forms/SKILL.md
@@ -21,12 +21,62 @@
 
 `htmlFor` / `id` pairing — universal rule in [`accessibility.mdc`](../../rules/accessibility.mdc) (Form label association). Form-specific notes:
 
 - `FormGroupLabel` → control `id` (or stable `name` when that is your field’s id convention).
-- Checkbox, Radio, Select: same pairing; checkbox/radio use the visually hidden input pattern from `@codecademy/gamut-styles` where applicable.
+- Checkbox, Select: same pairing; checkbox uses the visually hidden input pattern from `@codecademy/gamut-styles` where applicable.
+- Radio: see [`Radio` and `RadioGroup`](#radio-and-radiogroup) below — its label/group wiring differs from a single Checkbox/Select field.
 
 ---
 
+## `Radio` and `RadioGroup`
+
+`packages/gamut/src/Form/inputs/Radio.tsx` · `packages/gamut/src/Form/inputs/RadioGroup.tsx` · reference: `packages/styleguide/src/lib/Atoms/FormInputs/Radio/Radio.mdx`
+
+### No `variant`/`size` props — state is derived, not chosen
+
+`Radio` does not take a consumer-facing `variant` or `size` prop. Its visual state is derived internally from the booleans you already pass:
+
+- `error?: boolean` and `disabled?: boolean` (from `BaseInputProps`) feed a `conditionalStyleState(error, disabled)` helper that resolves to `'error' | 'disabled' | undefined`, which is then applied via `variant()` (from `@codecademy/gamut-styles`) on both the input and its label (`conditionalRadioInputStyles` / `conditionalRadioLabelStyles` in `Form/styles/Radio-styles.ts`).
+- `error` wins over `disabled` when both are true — do not pass both expecting a combined visual; pick the one that matches the actual field state.
+- There is no size scale — all Radio inputs render at a fixed 20×20 indicator. If a design calls for a different size, that's a design deviation to flag, not a prop to reach for.
+
+### Visually-hidden-input pattern
+
+The real `<input type="radio">` (`RadioInput`) is styled with `screenReaderOnly` from `@codecademy/gamut-styles` — it stays in the accessibility tree and keeps native keyboard/focus/change behavior, but is visually hidden. The circle indicator the user sees is rendered on the sibling `RadioLabel`'s `::before` (outer ring) and `::after` (fill dot) pseudo-elements, driven by CSS sibling selectors keyed to the hidden input's `:checked`, `:hover`, and `:focus` state (`InputSelectors.CHECKED_AFTER`, `HOVER_FOCUS_BEFORE`, etc.).
+
+Do not rebuild this with a `className`/inline-`style` override or a separate decorative `<span>` — that duplicates state the hidden input already owns and drifts out of sync on focus/hover/checked. If a design needs a different indicator look, change `Radio-styles.ts`, not the call site.
+
+### Group and label wiring
+
+`RadioGroup` clones its `Radio` children and injects three things you should _not_ also pass on each child: `name`, `onChange`, and `htmlFor` (built from `htmlForPrefix` + child index, e.g. `example-radio-0`). Wire it as:
+
+- `RadioGroup` gets `htmlForPrefix`, `name`, and (optionally) `onChange`/`disabled`/`selected`.
+- Each `Radio` gets its own `label`, `value`, and optionally `infotip` — not `name` or `htmlFor`, those come from the parent.
+- To label the group itself (not an individual option), wrap `RadioGroup` in `FormGroup` and pass `infotip` to `FormGroup`, not to a `Radio` — `FormGroup` links it via `aria-labelledby` to the group label automatically. Individual `Radio`s can still carry their own `infotip` for per-option context; the two are independent (see the worked example below).
+
+### Example — `RadioGroup` inside `FormGroup`, with mixed InfoTips
+
+```tsx
+<FormGroup
+  htmlFor="plan-select"
+  infotip={{ info: 'Applies to the whole group', placement: 'floating' }}
+  label="Select a plan"
+>
+  <RadioGroup htmlForPrefix="plan-select" name="plan-select">
+    <Radio
+      label="Pro"
+      value="pro"
+      infotip={{ info: 'Includes priority support' }}
+    />
+    <Radio label="Basic" value="basic" />
+  </RadioGroup>
+</FormGroup>
+```
+
+For an error state on the group, pass `error` to each `Radio` (there is no group-level `error` prop on `RadioGroup` itself) and surface the message the same way as any other field — via `FormGroup`'s `error` prop, not a second live region (see "Live regions — do not double up" below).
+
+---
+
 ## `FormGroup` (baseline)
 
 `packages/gamut/src/Form/elements/FormGroup.tsx`