# Atoms — Frame toggle

A two-option **segmented control** for switching a bike's frame type (e.g. *Hoge instap* vs *Lage
instap*). Built from Gazelle's **Tailwind v4 utilities**: each option is a `<label>` wrapping a
visually-hidden native `<input type="radio">` (`peer sr-only`) plus a drawn segment holding a
frame-type icon, so keyboard, focus and form semantics come from the browser. The selected option
rides a white "thumb" outlined in brand blue over an oyster track. The copy field on the Atoms →
Frame toggle screen emits this exact markup. A framework-free `.gz-frame-toggle` API in
`components/toggle.css` mirrors the same design for projects not on Tailwind.

## Metadata

- **Category:** Atoms
- **Status:** Stable
- **Source:** `tailwind.css` (theme utilities) + `docs/atom-classes.js` (canonical class lists and
  `GZ_ATOM_MARKUP.frameToggle`, shared with molecules) + `docs/atoms.js` (screen);
  `components/toggle.css` (`.gz-frame-toggle` framework-free compatibility API)
- **Figma:** Bike Cards 1.5, node `6517:33147`

## When to use

- Switching a single value between exactly **two** mutually-exclusive frame types on a product
  card. Wrap the options in a container with `role="radiogroup"`, give both `<input>`s the same
  `name`, and let the browser enforce single-select and arrow-key navigation.
- The interactive state is driven entirely by the real `<input>`: the white pill and its brand-blue
  outline both follow from `peer-checked:`; the segment takes a focus outline on
  `peer-focus-visible:`. Never toggle classes by hand.
- The track lifts one oyster step on `hover:` (500 → 800); this is a container-level hover, not a
  per-option one.

## When not to use

- Not for on/off state — that is a switch/checkbox, not a two-icon frame picker.
- Not for three or more frame types; use a wider segmented control or a select.
- Never drop the native `<input>`. The segments and icons are decorative; screen-reader and
  keyboard support depend on the real radios being present — visually hidden via `sr-only`, not
  `hidden`.
- In a Tailwind project, don't reach for the `.gz-frame-toggle` compatibility classes — apply the
  utilities so the copied class list stays the single source of truth.

## Anatomy

```html
<div class="inline-flex items-center rounded-full bg-surface-oyster hover:bg-oyster-800"
     role="radiogroup" aria-label="Frame type">
  <label class="group relative flex cursor-pointer">
    <input class="peer sr-only" type="radio" name="frame-type" checked aria-label="Lage instap" />
    <span class="<segment>">
      <svg class="<icon>" viewBox="0 0 24 24" fill="none" stroke-width="1" aria-hidden="true"><!-- gz-frame-type-low --></svg>
    </span>
  </label>
  <!-- one <label> per frame type -->
</div>
```

Small size swaps the segment to `h-10 w-[60px]` and the icon to `size-8` (`stroke-width="1.15"`).

**Responsive size step.** `lg-tablet` keeps the Small size and adds `tablet:h-11 tablet:w-[66px]`
(segment) and `tablet:size-9` (icon), so the toggle is Small below tablet and Large from tablet.
The stroke stays at the Small weight (`1.15`), because an SVG attribute can't change per
breakpoint. The Bike card uses this step (production `FrameSelector`: 36 → 40px icon, 12 → 13px
padding). Give each `<input>` a `value` (the frame identifier, e.g. `frame-low`) when a script
needs to read the choice, and a `name` unique to its card.

## Class lists

| Part | Utilities |
| --- | --- |
| **Track** | `inline-flex items-center rounded-full bg-surface-oyster transition-colors duration-[120ms] hover:bg-oyster-800` |
| **Option (label)** | `group relative flex cursor-pointer` |
| **Input** | `peer sr-only` |
| **Segment** | `flex items-center justify-center rounded-full border border-transparent text-fg-primary h-11 w-[66px] transition-[background-color,border-color] duration-[120ms] peer-checked:border-surface-brand peer-checked:bg-white peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-focus-ring` (small: `h-10 w-[60px]`) |
| **Icon** | `size-9 shrink-0` (small: `size-8`); `viewBox="0 0 24 24"`, `stroke-width="1"` (small `1.15`) |

## States (from Figma)

| State | Look | Driven by |
| --- | --- | --- |
| Default (unselected segment) | transparent (oyster track shows through), dark icon | base classes |
| Selected | white pill outlined in brand blue, dark icon | `peer-checked:` (`bg-white` + `border-surface-brand`) |
| Hover | track lifts oyster 500 → 800 | `hover:` on the track |
| Focus | outline ring on the segment for keyboard users | `peer-focus-visible:` |

## Icons

- Two frame-type glyphs from the **icon package** — `gz-frame-type-low` (diamond / full triangle)
  and `gz-frame-type-high` (with the extra lower bar) — inlined from
  `docs/gazelle-icons-data.js`, the same way the checkbox reuses the package's `check` glyph. They
  share the package's 24×24 grid; a third `gz-frame-type-mid` variant exists but this toggle only
  uses low/high.
- The stroke is `currentColor` and resolves to `text-fg-primary` in both segments — selection is
  signalled by the white pill and its blue outline, not by the icon colour. No hex is baked into
  the markup.
- `stroke-width` is tuned per size (`1` large, `1.15` small) so the scaled-up glyph keeps the
  package's ~1.5px optical stroke weight.
- Icons are inlined (not `<img>`) so they inherit `currentColor` and stay crisp at any size.

## Tokens & utilities

- **Track** — `bg-surface-oyster` (= Main/Oyster/500), `hover:bg-oyster-800` (= Main/Oyster/800).
- **Thumb** — `bg-white` with a `border-surface-brand` (= Main/Blue/500) outline; a base
  `border-transparent` keeps the box the same size in both states.
- **Icon colour** — `text-fg-primary` (both segments); `outline-focus-ring` (focus).
- **Geometry** — `h-11 w-[66px]` / `h-10 w-[60px]` segment (the track has no padding — segments
  fill it), `size-9` / `size-8` icon (on the package's 24×24 grid) are fixed sizes against Figma,
  not `--space-*` steps.

## Common mistake

Adding the selected `border` only on `peer-checked:` without a matching `border-transparent` in the
base — the 1px border then shifts the layout when selection changes. Keep the border in both states
and only change its colour. Likewise, never toggle a class to fake "selected"; the white pill and
blue outline must follow the real radio via `peer-checked:`.

## Cross-references

- `specs/foundations/color.md` — the oyster track, white thumb and icon colour roles.
- `specs/atoms/swatch.md` — the other radiogroup-based single-select atom; contrast its colour dot
  with the toggle's icon-plus-thumb treatment.
- `specs/atoms/choice.md` — shares the visually-hidden native input pattern.
- `specs/icons/icons.md` — the `gz-frame-type-low` / `gz-frame-type-high` glyphs the toggle reuses.
