# Slider (`slider`)

Native <input type=range> — styled track + thumb, both themes, with an editable value companion via a tiny delegated controller.

> **Layer:** L1 surface · **Recipe:** _(unset — see docs/agent/pick-a-surface.md)_
> Curriculum: `AGENTS.md` · pick matrix: `docs/agent/pick-a-surface.md` · blast radius: `CONSUMER_MAP.md`

> **Dialect:** Partial below is **unprefixed** (gallery / standalone HM). DOM contract Python often uses the **source token** `data-dz-*` / `dz-*` (Dazzle dual-lock). Match the CSS/JS bundle you load.

> **Demo vs contract:** Live gallery behaviour may use `/mock/*` or flash toasts. Those are **offline demos only** — implement **Server exchange** + **DOM contract**, not the mock. See AGENTS.md › Gallery demos.

## Copy this

```html
<div class="hm-stack hm-measure">
  <label class="form-label" for="hm-slider-vol">Volume</label>
  <div class="form-slider-group"><input id="hm-slider-vol" type="range" data-slider class="form-slider" min="0" max="100" step="1" value="70"><input data-range-value class="form-slider-value" type="text" inputmode="decimal" spellcheck="false" autocomplete="off" aria-label="Slider value" value="70"></div>
</div>
```

## Server exchange

This Hyperpart has **no server exchange** — presentation or client chrome only. If you put `hx-*` on a control that uses this markup, that action's exchange belongs to the action, not this part.

## Swap contract

Agent-visible HTMX topology (ADR-0054 / decision 0012). **Exchange envelope** = what the response may re-emit relative to the persistent slot (`body_only` | `outer` | `none` | `host_owned` | `document`). Dual-lock validates part markup only — not this envelope. Stem: `stems/morph-safe-hypermedia.md`.

**No host HTMX exchange** on this part — presentation or client chrome only. **Exchange envelope:** `n/a`.

If a **host** wraps this markup in `hx-*`, **that host owns the swap contract** (sole identity + envelope). Prefer `innerMorph` / `outerMorph` for stable slots; replacement for flash; body-only responses under inner swaps.

### Envelope response examples

What the **server returns** for each exchange. Match the **exchange envelope**; dual-lock still applies to interior markup.

This part has no owned exchange (envelope `n/a`). If a host adds `hx-*`, that host’s envelope applies — typically `body_only`:

```html
<!-- Prefer hx-swap=innerMorph into a stable body slot -->
<div class="dz-stack">content…</div>
```

Do **not** re-own the slot:

```html
<div id="slider-root" data-dz-region>…</div>
```

## How to use it

### Seams

- native <input type=range> is the submitted value; [data-dz-range-value] is the editable companion
- each slider group is scoped so many coexist on one page

### Do / Don't

| Do | Don't |
|---|---|
| write a valid companion number into the range; refuse leftover junk | replace the native range with a div-based slider |

### Pitfalls

- leftover junk in the companion must not invent a range position
- do not invent a custom thumb/track in JS; style the native control

### Keyboard / AT

- Arrow keys adjust the native range (browser default)
- companion has aria-label; leftover junk fails custom validity
- focus ring is theme-aware on the track/thumb

### Related parts

- `field` — agents/field.md

## DOM contract

What emitted markup must satisfy (CI: `tests/test_contracts.py`). Do not invent attrs outside the tables. Python modules under `contracts/` are **package-internal dual-locks** (`from contracts._kit import …`) — not FastAPI business handlers. App servers implement **Server exchange** endpoints; this section constrains the HTML those endpoints return.

### `contracts/slider.py`

- **Required root:** `[data-dz-slider]` (part `slider`)

| Node | Attr | Constraint |
|---|---|---|
| `[data-dz-slider]` | `—` | — |
| `[data-dz-range-value]` | `—` | — |

#### Module source

Monorepo dual-lock only — import `contracts._kit` from the HM package. Do not paste into app route modules.

```python
"""HYPERPART: slider — native range group + editable value companion.

Leftover honesty (cycle 2134): the readout is an editable text input
(no ``name`` — the native ``type=range`` is the submitted value).
Typed leftover junk (``70abc``, ``zzz``) must not invent a range
position — the range stays put and both controls fail custom validity
so submit cannot post the previous value as if the leftover were
accepted. Empty companion on blur restores from the range.
"""

from contracts._kit import DomContract, Node

DOM_CONTRACT = DomContract(
    part="slider",
    root="[data-dz-slider]",
    nodes=(
        Node("[data-dz-slider]", attrs={}),
        Node("[data-dz-range-value]", attrs={}),
    ),
)

__all__ = ["DOM_CONTRACT"]
```

## Notes

The track + thumb are styled for both themes with a focus ring. The native range is the submitted value (the companion has no name). dz-slider.js mirrors both ways, scoped to each slider's own group so many coexist. Leftover junk in the companion must not invent a range position (cycle 2134).

## Source files

- `site/registry.py` (partial + exchanges + guidance)
- `contracts/slider.py`
- `controllers/dz-slider.js`
