Liqui Design

Slider

A convex-lens thumb that magnifies the rail it rides, swelling out of it when grabbed.

Loading…
Grab the thumb and hold. It swells to nearly twice its size, its white fill drops to a tenth, and the rail visibly fattens as it passes behind the glass.

Installation

npx shadcn@latest add https://liqui.design/r/slider.json

Usage

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.

ProfileError against the reference's own map
Squircle, best thickness1.84px RMS
Circle, best thickness4.54px RMS
Superellipse p = 3.250.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 it

Two 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.

Range

Loading…

On this page