Playground
A range control with a chunky pixel thumb. It wraps a native <input type="range">, so keyboard, touch and screen readers work out of the box. It exposes a numeric value property for data-bind.
Whichever way a value arrives (the attribute, the property, data-bind), it is put in range and on a step, like the native input's thumb. With step="0" the slider is continuous and shows two decimals.
Examples
Basic
<div style="display: grid; gap: 20px; inline-size: min(100%, 22rem)">
<sb-slider label="Thrust" value="72" unit="%"></sb-slider>
<sb-slider label="Heading" min="-180" max="180" value="30" unit="°" ticks></sb-slider>
<sb-slider label="Precision" min="0" max="1" step="0.01" value="0.5"></sb-slider>
</div>
Steer anything with signals
Declare a signal, bind the slider to it, and drive other elements' attributes with data-attr. Nothing goes to the server: signals that start with _ stay in the browser.
<div data-signals:_size="48" style="display: grid; gap: 16px; inline-size: min(100%, 22rem)">
<sb-slider label="Size" min="16" max="96" unit="px" data-bind:_size__prop.value></sb-slider>
<img src="/art/info.svg" alt="" data-attr:width="$_size" data-attr:height="$_size" style="image-rendering: pixelated">
</div>
When the driven element is a component inside a server-rendered region, add data-preserve-attr for those attributes. Then a server morph won't reset them:
<sb-gauge data-attr:value="$_fuel" data-preserve-attr="value"></sb-gauge>
With commands
Give it a name, and it emits sb-change with { name, value } when a value is committed: ready to post as a command. With confirm, it sets :state(pending) until the server's re-rendered attribute matches, and revert() goes back to the server's value when a command is rejected. Every step taken with the keyboard is a commit, and the server echoes each one: with confirm, the echo of an older commit doesn't pull the thumb back while newer ones are on their way, so holding an arrow key loses no steps. Any other new value from the server wins, even value="0" on a slider first rendered without one. See Commands and components and the Showcase.
Forms
Inside a <form>, sb-slider submits its value under its name (name=72: the number, without the unit), new FormData(form) and Datastar's contentType: 'form' include it, and a form reset brings back the server's value. A disabled slider submits nothing. It is not a form-associated element yet (Rocket can't declare one), so required and validity, <fieldset disabled>, <label for> and the form attribute don't reach it. With commands, sb-change carries { name, value } (see With commands).
Styling
Style it from your page's CSS, without changing the component or importing anything into it. Custom properties, inherited properties and ::part() all reach into its shadow root.
- Size: it fills the width it is given (at least
8rem); setmax-inline-sizeon the element to cap it. - Direction: in right-to-left text the minimum is on the right, and the track fills from there.
- Fonts: the label, the value and the tick labels use your page's font.
- Colours: the filled part of the track is
--sb-brand, the rest--sb-surface-insetwith a--sb-borderedge. The thumb is--sb-slider-thumb(default--sb-text-1) with a--sb-slider-thumb-edgeedge (default--sb-brand-light), which is also the focus ring. The label is--sb-text-2, the value--sb-text-1, the ticks--sb-text-muted.--sb-notch: 0rounds the track and the thumb instead of notching them. - Parts:
label,valueandinput(the range input). The thumb is drawn inside the input and has no part: colour it with--sb-slider-thumband--sb-slider-thumb-edge. Your page's::part()rules win over the component's own, without!important.
<style>
.my-slider { max-inline-size: 20rem; --sb-brand: #F97316; --sb-brand-light: #FDBA74; --sb-notch: 0; --sb-slider-thumb: #FFFFFF; --sb-slider-thumb-edge: #F97316; }
.my-slider::part(value) { color: #F97316; }
</style>
<sb-slider class="my-slider" label="Thrust" value="70" unit="%"></sb-slider>
Accessibility
The native range input provides the role, value and keyboard support (arrow keys, Page Up/Down, Home/End). Its name is the label (a click on the label focuses the slider), else the element's aria-label, else "Value": give it one of the first two. The value is announced with its unit (aria-valuetext, e.g. "30°"). With forced colours (e.g. Windows High Contrast) it shows the native control, drawn in the system's colours.
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 a Datastar morph adds later. The Datastar here is v1.0.4 with Starbase's fixes until they are released; the components also run on the official release.
<script type="importmap">
{ "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>
<sb-slider label="Volume" value="60" unit="%"></sb-slider>No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.
Load just this component, pinned to this version: all its modules in one minified file with its integrity hash, so the browser refuses it if a single byte changes.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/slider@a9507a315024/slider.bundle.min.js" integrity="sha384-5bLKzT9WYDfcjUBgr28q+sOPL8+u8GzAd+bx1fZDwhX0FOm0ALp63APwJPwDzab5"></script>
<sb-slider label="Volume" value="60" unit="%"></sb-slider>Or the minified module files (one file, which the browser requests as it finds their imports), each pinned in the import map:
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/slider@a9507a315024/slider.min.js": "sha384-KkLdbkLQbXpIrLKHLYZ42So+rAmfFInoUmqekuIlsUvWo1KtUUuDbybBqiHV0Ld1"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/slider@a9507a315024/slider.min.js" integrity="sha384-KkLdbkLQbXpIrLKHLYZ42So+rAmfFInoUmqekuIlsUvWo1KtUUuDbybBqiHV0Ld1"></script>
<sb-slider label="Volume" value="60" unit="%"></sb-slider>In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.
<script type="importmap">
{
"imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
"integrity": {
"https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
"https://starbase.zweiundeins.gmbh/c/slider@a9507a315024/slider.bundle.min.js": "sha384-5bLKzT9WYDfcjUBgr28q+sOPL8+u8GzAd+bx1fZDwhX0FOm0ALp63APwJPwDzab5"
}
}
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@339249aca48f/autoloader.js" integrity="sha384-ZNpMayI1XShztIEX0iDHyCDDbhT8H+t+zEESUq4BCrN4b+7m931LQEl5x0vbY+Vo"></script>
<sb-slider label="Volume" value="60" unit="%"></sb-slider>Components import only datastar and their own files, so copying is enough: save each component's one file (and a file it loads on first use, keeping its folder), and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).
<sb-slider>slider.bundle.min.jsone file 1.9 kB brotli
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/slider/slider.bundle.min.js"></script>
<sb-slider label="Volume" value="60" unit="%"></sb-slider>Or copy the module files, keeping each component's folder, and load each component's module:
<script type="importmap">
{ "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/slider/slider.min.js"></script>
<sb-slider label="Volume" value="60" unit="%"></sb-slider>Size
A page loads this component as one file, its bundle, compressed the way it is served (gzip -9, brotli -11): brotli is what it downloads. Datastar and Rocket are shared by every component and not counted.
| File | Original | gzip | brotli |
|---|---|---|---|
slider.bundle.min.js | 4.9 kB | 2.2 kB | 1.9 kB |
As module files: one request, 1.9 kB minified with brotli
For a page that loads the module files as they are, requesting each one as it finds its import, or bundles them itself. Each file compressed on its own; the minified one is what the page downloads, and the readable source is always there too.
| File | Original | gzip | brotli | minified |
|---|---|---|---|---|
slider.js | 10.8 kB | 4.2 kB | 3.6 kB | 1.9 kB |
API reference
Props
| Attribute | Type | Default | Description |
|---|---|---|---|
value | number | 0 | The value. A new value from the server replaces it; the live value is the value property. |
min | number | 0 | Minimum. |
max | number | 100 | Maximum. |
step | number | 1 | Step size (0 for continuous). |
label | string | "" | Visible label. |
unit | string | "" | Suffix after the shown value, e.g. "°" or "%". |
show-value | boolean | true | Show the current value next to the label. |
ticks | boolean | false | Show min and max below the track. |
disabled | boolean | false | Disable interaction. |
confirm | boolean | false | Server-confirmed value: :state(pending) while the local value differs from the server's value attribute (see revert()). |
name | string | "" | Name reported in sb-change (e.g. the field of a command) and submitted with a form. |
Events
| Name | Description |
|---|---|
input | While dragging (native, re-targeted to the host). |
change | When the value is committed. |
sb-change | When the value is committed. detail: { name, value }: ready for a command. |