Range
A range is a min–max slider: two native <input type="range"> overlaid on one
track. Each carries .re-slider, so the thumbs look exactly like a single
slider. With zero JS both inputs work, are keyboard-operable, and submit
their own name=value. enhanceRange keeps the thumbs from crossing, draws the
fill between them, and routes a track click to the nearer thumb.
import { enhanceRange } from "@relements/core/behaviors/range";enhanceRange(document);<fieldset
class="re-range"
data-re-range
style="--re-range-fill-start: 20%; --re-range-fill-end: 70%"
>
<legend class="re-field__label">Price range</legend>
<div class="re-range__track">
<input
type="range"
class="re-slider re-range__input"
data-re-range-min
min="0"
max="1000"
step="10"
value="200"
aria-label="Minimum price"
name="price-min"
/>
<input
type="range"
class="re-slider re-range__input"
data-re-range-max
min="0"
max="1000"
step="10"
value="700"
aria-label="Maximum price"
name="price-max"
/>
</div>
<output class="re-range__output" aria-hidden="true">200 – 700</output>
</fieldset> data-size="sm|lg" on the .re-range mirrors the slider sizes.
<fieldset
class="re-range"
data-size="sm"
data-re-range
style="--re-range-fill-start: 30%; --re-range-fill-end: 60%"
>
<legend class="re-field__label">Small</legend>
<div class="re-range__track">
<input
type="range"
class="re-slider re-range__input"
data-re-range-min
min="0"
max="100"
value="30"
aria-label="Small minimum"
/>
<input
type="range"
class="re-slider re-range__input"
data-re-range-max
min="0"
max="100"
value="60"
aria-label="Small maximum"
/>
</div>
</fieldset>
<fieldset
class="re-range"
data-size="lg"
data-re-range
style="--re-range-fill-start: 10%; --re-range-fill-end: 50%"
>
<legend class="re-field__label">Large</legend>
<div class="re-range__track">
<input
type="range"
class="re-slider re-range__input"
data-re-range-min
min="0"
max="100"
value="10"
aria-label="Large minimum"
/>
<input
type="range"
class="re-slider re-range__input"
data-re-range-max
min="0"
max="100"
value="50"
aria-label="Large maximum"
/>
</div>
</fieldset> Disabled
Section titled “Disabled”<fieldset
class="re-range"
data-re-range
disabled
style="--re-range-fill-start: 25%; --re-range-fill-end: 75%"
>
<legend class="re-field__label">Unavailable</legend>
<div class="re-range__track">
<input
type="range"
class="re-slider re-range__input"
data-re-range-min
min="0"
max="100"
value="25"
aria-label="Disabled minimum"
/>
<input
type="range"
class="re-slider re-range__input"
data-re-range-max
min="0"
max="100"
value="75"
aria-label="Disabled maximum"
/>
</div>
</fieldset> - Crossing is prevented by clamping the moved input’s value — both inputs
keep their authored
min/max(the full range), so each thumb maps to the same track andaria-value*stays honest. data-re-range-gapsets a minimum distance between thumbs (default =step).- When the two thumbs fully coincide, separate them with the keyboard (Tab to a thumb, then arrow) — the focused thumb sits on top.
- Native range thumb mirroring under
dir="rtl"is engine-dependent; verify the fill alignment if you ship RTL.
Accessibility
Section titled “Accessibility”The range is two native <input type="range">, so each thumb is an independent,
fully native slider — enhanceRange adds no role or aria-*. It only clamps
the moved input’s value (keeping each thumb’s aria-valuemin/max/now
honest), draws the fill, and routes track clicks. Everything below works with
zero JS.
- Keyboard — native per thumb:
Tabfocuses a thumb, arrow keys nudge bystep,Page Up/Page Downjump a larger increment, andHome/Endgo to that input’smin/max. When the two thumbs fully coincide,Tabto one and arrow it off — the focused thumb sits on top (z-index) so it stays grabbable. - Focus — each thumb shows a visible
:focus-visiblering (--re-shadow-focuson the thumb pseudo-element; the native outline is removed, not dropped). The focused thumb is raised above the other so its ring is never clipped. Disabling the wrapping<fieldset>propagates:disabledto both inputs (and dims the fill), taking them out of the tab order. - Semantics — both inputs keep the native
sliderrole and surface their ownaria-valuemin/max/nowfrom the authoredmin/max/value. The<fieldset>/<legend>names the group, and each input also carries anaria-label(e.g. “Minimum price” / “Maximum price”) so the two thumbs are told apart. The.re-range__outputreadout isaria-hidden="true"— it duplicates the live thumb values, which AT already announces, so hiding it avoids a double report. - Notes — the track and fill band are CSS pseudo-elements with no role, so
the measurement is never carried by color alone. Each thumb is a real native
range input, so it stays operable under
forced-colors: active(Windows High Contrast). See the slider and field pages and the accessibility guide.