# Atom — Slider

A dual-thumb range control built from two native `<input type="range">` elements. It selects a
lower and upper bound while retaining browser keyboard and form semantics. The Atoms → Slider
screen emits Tailwind v4 HTML; `components/slider.css` provides the `.gz-slider` compatibility API.

## Metadata

- **Category:** Atoms
- **Status:** Stable
- **Source:** `tailwind.css` + `docs/atoms.js`; `components/slider.css` for framework-free projects
- **Figma:** MOP 1.5, node `8548:22682`

## When to use

- Use Slider when people benefit from choosing an approximate interval visually.
- Use the lower input for the minimum and the upper input for the maximum.
- Keep both inputs in the DOM and give each a distinct accessible name.
- Give both inputs the same `min`/`max`; clamp the values in JS so they cannot cross.
- Use explicit fields instead when users must enter an exact value.

## Anatomy

```html
<div class="<slider root>">
  <div class="<muted track>">
    <span class="<selected range>" style="left: 30%; width: 40%"></span>
  </div>
  <input class="<range input>" type="range" min="0" max="100"
         value="30" aria-label="Minimum price" />
  <input class="<range input>" type="range" min="0" max="100"
         value="70" aria-label="Maximum price" />
</div>
```

Both inputs share the **same** `min`/`max` (the control's full range) so their thumbs map
linearly onto the same track and stay aligned with the selected span. The selected-range
position is derived from the two values. In a product framework, update its `left` and `width`
whenever either controlled input changes, and enforce the no-cross rule in your change handler
(clamp lower to ≤ upper and upper to ≥ lower) — never by pinning one input's `min`/`max` to the
other's current value.

## Geometry

| Part | Value |
| --- | --- |
| Component | 238 × 48px |
| Track | 4px high; inset 10px from both sides |
| Thumb | 20 × 20px |
| Track radius | Full |

The 48px component height supplies the interaction target while the visible thumb remains 20px.

## States from Figma

| Preset | Lower | Upper |
| --- | ---: | ---: |
| Every price | 0 | 100 |
| Cheap | 0 | 50 |
| Expensive | 50 | 100 |
| Middle | 30 | 70 |

These are showcase presets, not separate component variants. Product code may use any ordered
values inside its own minimum and maximum.

## Tokens and assets

- Selected track and thumb: `surface-brand` / Gazelle blue.
- Unselected track: `surface-brand` at 10% opacity.
- Track radius: `rounded-full`.
- Thumb asset: `assets/slider-thumb.svg`, the exact 20px circle exported from Figma.

## Implementation notes

- Both `<input type="range">` elements keep the **same fixed `min`/`max`** (the control's
  full range). Do not pin one input's `min`/`max` to the other's current value: a range
  thumb is positioned as `(value − min) / (max − min)`, so changing one input's `min`/`max`
  reprojects its thumb non-linearly and makes dragging one handle visibly drag the other,
  and also pulls both thumbs out of alignment with the selected span. Enforce the no-cross
  rule in JS instead (clamp `lower ≤ upper` and `upper ≥ lower`).
- Both `<input type="range">` elements are `pointer-events: none` on the host, with
  `pointer-events: auto` re-enabled only on the `::-webkit-slider-thumb` /
  `::-moz-range-thumb` pseudo-elements. This keeps each input clickable only at its own
  visible thumb, so the two overlapping inputs never fight over clicks meant for the
  other's track.
- Never re-enable `pointer-events: auto` on `:focus` (or any other state) for the whole
  host input. Doing so expands the clickable hit area to the entire 238×48px box, which
  — combined with the other input's default stacking — makes the other handle
  permanently undraggable once the first has been focused. Only bump `z-index` on
  focus/near-collision, never `pointer-events`.
- The two thumbs can end up at the exact same value (dragged together). Since only the
  thumb pixels are hit-testable, this is the *only* case where stacking order matters.
  Bump whichever input has crossed the midpoint of its own travel (`lower > 50` /
  `upper < 50`) to `z-index: 3` so it stays reachable from either side.

## Accessibility

- Use native range inputs; never replace them with pointer-only draggable elements.
- Give the inputs contextual accessible names such as “Minimum price” and “Maximum price”.
- Preserve keyboard focus and arrow-key behavior.
- Expose the product's real numeric `min`, `max`, `step`, and `value`.

## Cross-references

- `specs/foundations/color.md` — semantic track and focus colors.
- `specs/atoms/choice.md` — another native-input atom with custom visuals.
