Slider
A control for picking a number by dragging it.
src/components/rahti_ui/slider.rs
Installation
Install this component in your Rahti 0.0.58 application. Class conflicts are resolved with the existing frontend `twMerge`; no Rust class-merging dependency is needed.
Basics
Drag one, or focus it and press an arrow key, `Home`, `End` or `Page Up` — none of that is written anywhere in this library. The thumb moves with no JavaScript at all; the track behind it fills live in Firefox for free, and in Chrome and Safari once the runtime has mounted. The section below says why the two engines are different.
Min, max and step
`min` and `max` are shadcn's own `0` and `100` by default, and both are always written onto the element even at those defaults — the fill expression reads them back off it, and an absent attribute would make the arithmetic `NaN`. The middle slider is the one to check: its value is `0` but its thumb is halfway, because half of `-50..50` is where `0` sits.
States
`disabled:opacity-50` is shadcn's `data-[disabled]:opacity-50`, read as the attribute a native input has. The second slider is the documented way out of the fill handler: it still drags, still posts, and on Firefox its track still fills — it is only Chrome and Safari that leave the paint behind.
With a Label
The tag form, which is what a slider this page does not drive should be. `Label`'s association does the same work here as on every other control, and the range input supplies its own value, minimum and maximum to assistive technology.
A `<Label for>` names the control, so a screen reader reads "Brightness, slider, 70" — the value comes from the element, not from an `aria-valuenow` anybody had to write.
As a form posts it
One `name`, and `?quality=4` in the query string — a range input always posts something, since it always has a value. No hidden field beside the control to carry it, and no JavaScript to put it there.
Attributes
`.attr(…)` writes anything; `.unset(…)` takes one away. `list` is worth knowing about — it is HTML's own tick marks, drawn by the browser from a `<datalist>`, and Radix has an equivalent only if you build one.
No visible label, so it carries an `aria-label` instead.
Class override
`class` is merged, not appended — but almost every utility in this component carries a pseudo-element prefix, and a merge only replaces a utility carrying the same prefix. So an override that means to reach the thumb has to name the thumb, and has to name it twice: `::-webkit-slider-thumb` and `::-moz-range-thumb` are the same thing to a reader and two unrelated selectors to a browser, which discards a whole rule it does not understand.
How the track gets filled
This is the one component in the library that asks for the runtime to finish looking like itself, so it is worth being precise about what that means.
| engine | what fills the track | with no JavaScript |
|---|---|---|
| Firefox | ::-moz-range-progress | Live. The browser owns the pseudo-element and keeps it in step itself. |
| Chrome, Safari | linear-gradient(…var(--slider-fill)) | Correct on arrival, then frozen. There is no progress pseudo-element on either, so the stop is a custom property an `oninput` handler moves. |
And the one rule that comes with it
The controller updates `--slider-fill` from the input value after hydration and parent-state changes. Its listener and observer are released when the component unmounts.
The fill follows the bound input value automatically. Dragging the thumb or changing parent state updates both the thumb and the track.
Why a binding and not an inline handler
Handlers are compiled by `html!` into the client registry. PulsePoint dispatches registered code under the application CSP without inline script execution or eval.
What still works with the runtime absent is the whole control: drag, arrow keys, `Home`/`End`, the value in the form post, and the announcement to a screen reader. Only the paint behind the thumb stops following, and only on two of the three engines.
What this port changed
shadcn's Slider is four elements — a Root, a Track, a Range that fills it, and a Thumb per value — over Radix's pointer maths. This is one `<input type="range">`, and the other three are pseudo-elements the class list reaches through.
| shadcn | here | why |
|---|---|---|
| Root / Track / Range / Thumb | one input, three pseudo-elements | Each of shadcn's utilities moves to the selector for the part it styled — twice for the parts the two engines name differently. |
| — | h-4 on the input | shadcn's Root is the height of its track and lets the Thumb overflow. An input clips its own thumb, so the input is the thumb's height and `-mt-[5px]` — which is `(6 - 16) / 2` — centres it on the 6px track. |
| value={[25, 75]} | — not ported | Radix renders a thumb per entry. One input has one value and one thumb, and there is no native two-ended range — two inputs overlaid plus the code to stop them crossing is a different component. |
| orientation="vertical" | — not ported | A native vertical range is `writing-mode: vertical-lr`, which is recent in every engine and needs a second copy of every track and thumb utility with its axis swapped. Worth doing; not done here. |
| — (Radix does it in JS) | drag, keys, touch, ARIA | Arrow keys, `Home`/`End`, `Page Up`/`Page Down`, a touch target the browser sizes for the device, and the value read out — none of it written. |
Everything else is shadcn's, copied rather than reinterpreted — including the `bg-white` thumb, which is white in both themes because shadcn's is.
The component tag, and its boundary
The tag is the right call for a slider this page drives as much as for one it does not — a `name`, a `value`, a `min` and a `max`. What a plain form posts, the tag renders.
Through the tag
A binding crosses the boundary: a component's root attributes are evaluated where the tag was written, so `value` and `oninput` read and write this page's state.
The fill follows the bound input value automatically. Dragging the thumb or changing parent state updates both the thumb and the track.
Controlled, page scope
The fill follows the bound input value automatically. Dragging the thumb or changing parent state updates both the thumb and the track.
{volume >= 80 ? "Loud." : volume >= 30 ? "Comfortable." : "Quiet."}
Both buttons move the thumb and the fill with it, because `value` is bound and the fill is derived from the same state. Drag the thumb and the readout follows; press a button and the thumb follows.
The fill follows the bound input value automatically. Dragging the thumb or changing parent state updates both the thumb and the track.