Slider
A convex-lens thumb that magnifies the rail it rides, swelling out of it when grabbed.
Installation
npx shadcn@latest add https://liqui.design/r/slider.jsonUsage
import {
Slider,
SliderControl,
SliderLabel,
SliderThumb,
SliderTrack,
SliderValue,
} from '@/components/ui/slider';<Slider defaultValue={62}>
<div className="flex items-baseline justify-between">
<SliderLabel>Volume</SliderLabel>
<SliderValue />
</div>
<SliderControl>
<SliderTrack>
<SliderThumb />
</SliderTrack>
</SliderControl>
</Slider>Two thumbs make it a range — see the range example.
Notes
Convex, where Switch is lipped
This and Switch are the same construction: a
purpose-built lens rather than a LiquiGlass surface,
sharing only the kernel in lib/lens.tsx. What differs between them is the
interesting part, and it is not cosmetic.
A switch thumb has nothing worth magnifying under it, so it takes a lip bezel — a raised rim around a flat window, which reads zoomed out. A slider thumb is sitting on the one thing in the control you actually want to look at: the rail, and the fill that says where the value is. So it takes a plain convex dome instead, and magnifies it. Grab one and the rail fattens as it crosses the glass.
The exponent is the tail
The dome is a superellipse, (1 − (1 − t)ᵖ)^(1/p), and p is not a
free-hand choice. It sets how fast the surface flattens as it leaves the rim,
which is to say how far inward the bend still reaches.
A squircle (p = 4) — the obvious pick, and what Switch's crest uses —
flattens too abruptly here: its slope collapses, the bend dies within a few
pixels of the edge, and the long reach this lens needs is not available at any
thickness. A circle (p = 2) misses the other end, at the rim. p = 3.25
holds both.
| Profile | Error against the reference's own map |
|---|---|
| Squircle, best thickness | 1.84px RMS |
| Circle, best thickness | 4.54px RMS |
| Superellipse p = 3.25 | 0.49px RMS |
Against a 70px peak — so 0.7%.
How hard the edge bends
The band is 42% of the lens's radius, and inside it the outermost pixels sample from more than a lens-height away — 118% of it. That is what turns the rail's straight edge into the curled lip of a real piece of thick glass rather than a soft smear, and it is far more than the switch asks for, because a slider has something behind it worth dragging into view.
The filter declares no region for the same reason Switch's does not: the SVG default is the bounding box plus 10%, and pinning it to the element would have Chromium clamp those long samples at the boundary.
Grabbed-ness comes from Base UI
The press state is read off Base UI's own data-dragging, watched with a
MutationObserver, rather than from pointer events on the thumb.
That is not a shortcut, it is the correct source. Base UI already starts a drag when you press the rail — the thumb jumps to your finger and follows it — and a thumb listening only to its own pointer events would sit there un-grabbed through the whole gesture.
The rail is flat
No inset groove, no glass. It is the thumb's backdrop, and a lens stacked on a lens samples its parent's output instead of the page. Whatever depth this control has comes from the thumb; a rail with its own shadow only competes with the lens passing over it.
The neutral grey is fixed rather than mixed from --lq-text, so it reads the
same on a white sheet and a dark wallpaper. Override --lq-slider-rail for the
rail and --lq-accent for the fill.
Turning the lens off
<SliderThumb lens={false} className="size-[18px]" />Refraction is not worth its cost for a slider sitting on another glass
surface — inside a popover, a drawer, a media-player panel — because
backdrop-filter samples what is painted behind the element, and behind a
thumb on a frosted panel is the panel's own tint: a flat wash with nothing in
it to bend. Without the lens the thumb takes its size from className, so a
panel can ask for a small knob.
Safari and Firefox take this path on their own — backdrop-filter: url(#…) is
Chromium-only.
Sizing
const RAIL_H = 14; // matches Meter and Progress
const LENS_H = 45; // the lens; everything optical is a ratio off itTwo knobs, because they are two decisions. The rail belongs to the
Meter/Progress family and is sized by them — it is the same h-3.5 they use, so
a slider read next to a meter looks like its sibling. The lens belongs to the
optics and is sized by how much glass the effect needs. Tying one to the other
means a slider that matches its siblings can only do so by carrying a knob half
again as tall as anything else on the page.
Everything else follows: the lens box is LENS_H at the reference's 3:2, its
radius is half of it, the resting knob is 60% of it, and the band and peak
displacement are ratios of the radius and the height.
The root publishes the layout sizes as custom properties (--lq-rail-h,
--lq-knob-w, --lq-knob-h, --lq-knob-pad) rather than inline styles, so the
rail and the hit area follow the constants while a caller's className="h-2"
still wins. The lens is the exception — its box is inline and not overridable,
because the maps are baked for exactly that size. For a smaller knob on a panel,
that is what lens={false} is for.
At rest the knob is 41×27, the same size as Switch's. Held, it is a 68×45 lens over a 14px rail — still more than three times the thing it sits on, which is the point. A lens the size of what it covers has nothing to show.