Slider
Picks a value from a range.
Installation
$ npx shadcn@latest add @constella/sliderUsage
import { Slider, SliderControl, SliderLabel, SliderValue,} from "@/components/primitives/slider";<Slider defaultValue={40}> <SliderLabel>Volume</SliderLabel> <SliderValue /> <SliderControl /></Slider>Composition
Slider├── SliderLabel├── SliderValue└── SliderControl └── SliderTrack ├── SliderIndicator └── SliderThumbRange
Pass an array to defaultValue and compose the track with a thumb per value.
<Slider defaultValue={[20, 80]}> <SliderLabel>Price</SliderLabel> <SliderValue /> <SliderControl> <SliderTrack> <SliderIndicator /> <SliderThumb index={0} /> <SliderThumb index={1} /> </SliderTrack> </SliderControl></Slider>Multiple Thumbs
Add a value and a SliderThumb for each thumb.
<Slider aria-label="Stops" defaultValue={[10, 40, 70]} step={10}> <SliderControl> <SliderTrack> <SliderIndicator /> <SliderThumb index={0} /> <SliderThumb index={1} /> <SliderThumb index={2} /> </SliderTrack> </SliderControl></Slider>Vertical
Use orientation="vertical" and give the slider a height.
const styles = stylex.create({ slider: { height: 160 },});
<Slider aria-label="Bass" defaultValue={60} orientation="vertical" sx={styles.slider}> <SliderControl /></Slider>;Steps
Set max and step, then place a mark under each step. The marks sit in a row inset by half a thumb, so each percentage lands under the thumb.
const months = Array.from({ length: 13 }, (_, month) => month);
const styles = stylex.create({ scale: { gridColumn: "1 / -1", height: space.lg, marginInline: `calc(${sizes.thumb} / 2)`, position: "relative", }, mark: (position: string) => ({ alignItems: "center", display: "flex", flexDirection: "column", gap: space.xxs, insetInlineStart: position, position: "absolute", translate: "-50% 0", }), tick: { backgroundColor: colors.border, height: space.xxs, width: strokes.border, }, tickMajor: { backgroundColor: colors.textMuted }, label: { color: colors.textMuted, fontSize: fontSizes.textSm, lineHeight: lineHeights.textSm, },});
<Slider defaultValue={3} max={12}> <SliderLabel>Duration (months)</SliderLabel> <SliderValue /> <SliderControl /> <div aria-hidden {...stylex.props(styles.scale)}> {months.map((month) => { const major = month % 2 === 0; return ( <span key={month} {...stylex.props(styles.mark(`${(month / 12) * 100}%`))} > <span {...stylex.props(styles.tick, major && styles.tickMajor)} /> {major && <span {...stylex.props(styles.label)}>{month}</span>} </span> ); })} </div></Slider>;Reference Labels
Add a row of labels under the control. A 1fr auto 1fr grid keeps the middle one centered.
const styles = stylex.create({ references: { color: colors.textMuted, display: "grid", fontSize: fontSizes.textSm, lineHeight: lineHeights.textSm, gridColumn: "1 / -1", gridTemplateColumns: "1fr auto 1fr", }, end: { justifySelf: "end" },});
<Slider defaultValue={20} max={35} min={5}> <SliderLabel>Storage</SliderLabel> <SliderValue>{(formatted) => `${formatted[0] ?? ""} GB`}</SliderValue> <SliderControl /> <div aria-hidden {...stylex.props(styles.references)}> <span>5 GB</span> <span>20 GB</span> <span {...stylex.props(styles.end)}>35 GB</span> </div></Slider>;Disabled
Use the disabled prop to disable the slider.
<Slider defaultValue={40} disabled> <SliderLabel>Volume</SliderLabel> <SliderValue /> <SliderControl /></Slider>Tooltip
Render the thumb as a Tooltip trigger with a SliderValue inside the content. Keep it open while dragging, since the pointer can leave the thumb.
const styles = stylex.create({ value: { color: "inherit", fontSize: "inherit" },});
function VolumeSlider() { const [hovered, setHovered] = useState(false); const [dragging, setDragging] = useState(false);
return ( <Slider defaultValue={40} onValueChange={(_, details) => { if (details.reason === "drag" || details.reason === "track-press") { setDragging(true); } }} onValueCommitted={() => setDragging(false)} > <SliderLabel>Volume</SliderLabel> <SliderControl> <SliderTrack> <SliderIndicator /> <Tooltip onOpenChange={setHovered} open={hovered || dragging}> <TooltipTrigger delay={0} render={<SliderThumb />} /> <TooltipContent> <SliderValue sx={styles.value}> {(formatted) => `${formatted[0] ?? ""}%`} </SliderValue> </TooltipContent> </Tooltip> </SliderTrack> </SliderControl> </Slider> );}Input Group
Control the slider with value and onValueChange, and keep a number Input Group in sync. The input holds a draft while typing and snaps back to the slider's value on blur.
const [value, setValue] = useState(100);const [draft, setDraft] = useState("100");
<Slider onValueChange={(next) => { setValue(next); setDraft(String(next)); }} value={value}> <SliderLabel>Opacity</SliderLabel> <InputGroup size="sm"> <InputGroupInput aria-label="Opacity" max={100} min={0} onBlur={() => setDraft(String(value))} onChange={(event) => { setDraft(event.target.value); const next = event.target.valueAsNumber; if (Number.isFinite(next)) { setValue(Math.min(100, Math.max(0, Math.round(next)))); } }} type="number" value={draft} /> <InputGroupAddon align="inline-end"> <InputGroupText>%</InputGroupText> </InputGroupAddon> </InputGroup> <SliderControl /></Slider>;API Reference
SliderControl renders the track, indicator and one thumb when it has no children. Every part takes sx, applied last. For the rest, see Base UI Slider.