Range

A two-thumb pixel slider for a start and an end, committed as one value.

zweiundeins <sb-range> range slider interval input form Since 2026-09-22 3.1 kB0Open in playground Edit on GitHub

Playground

A slider with two thumbs, for a start and an end: a price band, a time window, an altitude range. It is the sibling of sb-slider, and is built on two native range inputs, so arrow keys, Page Up/Down, Home/End and touch work on each thumb. The thumbs can meet but not cross.

The range is one value: value='{"start":20,"end":60}' in, { start, end } out. Both ends change together in one sb-change and one command. See Components with several values.

Examples

Basic

<div style="display: grid; gap: 20px; inline-size: min(100%, 22rem)">
  <sb-range label="Orbit" value='{"start":300,"end":800}' min="160" max="2000" step="10" unit=" km" ticks></sb-range>
  <sb-range label="Launch window" value='{"start":9,"end":17}' min="0" max="24" unit=":00"></sb-range>
  <sb-range label="Signal" value='{"start":0.2,"end":0.8}' min="0" max="1" step="0.01"></sb-range>
</div>

A missing part is the bound: value='{"end":40}' starts at min, and without a value the whole range is selected.

The whole range in one event

sb-change fires once per committed drag, with both ends:

<div data-signals="{_window: ''}" style="display: grid; gap: 12px; inline-size: min(100%, 22rem)">
  <sb-range name="window" label="Launch window" value='{"start":9,"end":17}' min="0" max="24" unit=":00"
    data-on:sb-change="$_window = JSON.stringify(evt.detail)"></sb-range>
  <code data-text="$_window || 'Drag a thumb…'"></code>
</div>

The live value is the value property: el.value returns { start, end }, and setting it accepts { start, end } or [start, end].

With commands

Post the detail as it is. The server validates the range as a whole and renders the new value attribute:

<sb-range name="window" confirm value='{"start":9,"end":17}' min="0" max="24"
  data-on:sb-change="@post('/cmd/window', {payload: {tabid: $tabid, ...evt.detail}})"
  data-on:datastar-fetch="evt.detail.el === el && evt.detail.type === 'error' && el.revert()"></sb-range>

With confirm, the host has :state(pending) while either end differs from the server's attribute, and revert() restores both ends. A command can never leave the server with only half a change. See Commands and components.

sb-range:state(pending) { opacity: 0.7; }

Styling

The thumbs and the fill follow --sb-text-1, --sb-brand-light and --sb-brand; --sb-notch: 0 gives rounded corners. Parts: label, value, rail, and input (plus start / end) for each native input.

Accessibility

The field is a group named by its label. Each thumb is a native slider, announced as "

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag. The autoloader imports each component the first time its tag appears, including tags added later by a Datastar morph.

<!-- Once per page: Datastar with Rocket, and the Starbase autoloader.
     It loads every <sb-…> component the first time its tag appears. -->
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<!-- Optional, no flash of undefined elements: class="sb-cloak" on <html>, and -->
<style>.sb-cloak :not(:defined) { visibility: hidden }</style>

<sb-range label="Orbit" value='{"start":300,"end":800}' min="160" max="2000" step="10" unit=" km" style="inline-size: 13rem"></sb-range>

<!-- In production, pin today's catalog instead of the latest: the browser then
     refuses any file that changed. Add "integrity" to the import map above: the
     hashes from https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/importmap.json and Datastar's, below. -->
<!--
<script type="importmap">
  { "imports": { "datastar": "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js" },
    "integrity": { "https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.4/bundles/datastar-rocket.js": "sha384-vUxZojLrF1Ar3de5h7VINqhJXBgjyZS4U49pHvGa87kum6j5Xn6JRziuARNw5ELG", "…": "…from importmap.json" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@0a1ad1bf98a4/autoloader.js" integrity="sha384-IvSCUjEWPS+wwXunU+48x96KmyWDx/Y4LeCwCL8cWFqJ/zHmTsTOsSMrzTOcpW3D"></script>
-->

<!-- Or load just this component, pinned to this version: -->
<!-- <script type="module" src="https://starbase.zweiundeins.gmbh/c/range@73041f207ebc/range.js" integrity="sha384-HewP4YrbHiRIdsdsTAatY04VNILm8Z+CgptW8QHBX9tl4H7qVsW3kiBGeuPfjL4H"></script> -->

Size

Each file compressed on its own, the way it is served (gzip -9, brotli -11). Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotli
range.js 9.6 kB3.6 kB3.1 kB

API reference

Props

AttributeTypeDefaultDescription
valueobject{ "start": null, "end": null }The range, as JSON: {"start": 20, "end": 60}; a missing part is min or max. A new value from the server replaces it; the live value is the value property.
minnumber0Minimum.
maxnumber100Maximum.
stepnumber1Step size (0 for continuous).
labelstring""Visible label; the thumbs are announced as "<label> start" and "<label> end".
unitstring""Suffix after the shown values, e.g. "°" or " kg".
show-valuebooleantrueShow the current range next to the label.
ticksbooleanfalseShow min and max below the track.
disabledbooleanfalseDisable interaction.
confirmbooleanfalseServer-confirmed value: :state(pending) while the local range differs from the server's value attribute (see revert()).
namestring""Name reported in sb-change (e.g. the field of a command).

Events

NameDescription
inputWhile dragging either thumb (native, re-targeted to the host).
changeWhen the range is committed.
sb-changeWhen the range is committed. detail: { name, value: { start, end } }: the whole range, one command.