Slider

Slider forms Controller

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.

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 — it is presentation or client chrome only. htmx does not issue a request on this part's behalf. If you put an affordance (hx-*) on a control that uses this markup, that action's exchange belongs to the action, not this part. See Swap contract for host-owned envelopes.

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; decision: docs/decisions/0012-swap-identity-contract.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

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

html
<!-- Host wraps this presentation part with hx-* (host owns envelope) -->
<!-- Prefer: hx-swap="innerMorph" hx-target="#panel-body" -->
<!-- Server returns body_only interior for #panel-body -->
<div class="dz-stack">content…</div>

Do not re-own the slot:

html
<!-- WRONG: server returns the presentation root with a new id every poll -->
<div id="slider-root" data-dz-region>…</div>

How to use it

Seams

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

Do / Don't

DoDon't
write a valid companion number into the range; refuse leftover junkreplace 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

DOM contract

What the emitted HTML must satisfy — the table is the required surface; Python under contracts/ is the package-internal dual-lock CI runs (tests/test_contracts.py), not an app route. Standalone HTMX4: implement the API so responses match this markup. Dazzle: the agent emits SSR that already satisfies it. Do not invent attrs outside these tables. For request/response wiring see Server exchange.

contracts/slider.py

Required in the DOM: root [data-slider] (part slider). Emit only these attributes — inventing extras is fine only if controllers ignore them; omitting required ones fails CI (tests/test_contracts.py).

NodeAttrConstraint
[data-slider]——
[data-range-value]——

Module source

Import path is monorepo/package-local (from contracts._kit import …). Source-token form often uses data-*; gallery demos above are unprefixed. Do not copy this into app routes.

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). 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

One logical Hyperpart, 4 code items (CSS layered, JS bundled). Bound by HYPERPART: slider — python tools/hyperpart.py slider lists them.

site/registry.py · contracts/slider.py · base/design-system.css:668 · controllers/dz-slider.js