Slider
An input component for selecting a value or range of values along a track.
Overview
The Slider component provides an accessible way for users to select a value or range of values along a continuous or discrete scale. Built on Base UI primitives, it offers full keyboard navigation, touch support, and optional tooltip displays.
Usage
import { Slider, SliderThumb } from "@tilt-legal/cubitt-components/primitives";<Slider defaultValue={[50]} max={100} step={1}>
<SliderThumb />
</Slider>Using Cubitt's URL-state hooks, you can sync the slider value with the URL by providing a paramName. The URL only updates when you release the slider thumb, not while dragging, to avoid performance issues:
// Slider value synced with ?volume=50
<Slider paramName="volume" defaultValue={[50]} max={100} step={5}>
<SliderThumb />
</Slider>
// Range slider synced with URL:
<Slider
paramName="price-range"
defaultValue={[100, 500]}
min={0}
max={1000}
step={10}
>
<SliderThumb />
<SliderThumb />
</Slider>
// Advanced options:
<Slider
paramName="brightness"
paramClearOnDefault={true} // Remove param when value equals default
onUrlValueChange={(value) => console.log('Brightness:', value)}
defaultValue={[75]}
max={100}
>
<SliderThumb />
</Slider>Examples
Default
import { Slider, SliderThumb } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<div className="w-full max-w-xs">
<Slider defaultValue={[50]} max={100} step={1}>
<SliderThumb />
</Slider>
</div>
);
}Range Slider
Use two thumbs to select a range of values.
import { Slider, SliderThumb } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<div className="w-full max-w-xs">
<Slider defaultValue={[100, 450]} min={0} max={600} step={1}>
<SliderThumb />
<SliderThumb />
</Slider>
</div>
);
}With Labels
Add labels to indicate the range or meaning of values.
import { Slider, SliderThumb, Label } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<div className="w-full max-w-xs">
<div className="flex flex-col gap-3">
<Label>Volume</Label>
<Slider defaultValue={[50]} step={10}>
<SliderThumb />
</Slider>
<span
className="text-muted-foreground flex w-full items-center justify-between gap-2 text-xs font-medium"
aria-hidden="true"
>
<span>Low</span>
<span>High</span>
</span>
</div>
</div>
);
}With Tooltip
Show the current value in a tooltip while dragging.
import { Slider, Label } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<div className="w-full max-w-xs">
<div className="flex flex-col gap-3">
<Label>Brightness</Label>
<Slider
defaultValue={[50]}
step={10}
showTooltip
aria-label="Slider with tooltip"
/>
</div>
</div>
);
}With Ticks
Display tick marks to indicate discrete values or steps.
import { Slider, SliderThumb, Label } from "@tilt-legal/cubitt-components/primitives";
import { cn } from "@tilt-legal/cubitt-components/utilities";
export default function Component() {
const max = 12;
const skipInterval = 2;
const ticks = [...Array(max + 1)].map((_, i) => i);
return (
<div className="w-full max-w-xs">
<div className="flex flex-col gap-3">
<Label>Slider with ticks</Label>
<div>
<Slider defaultValue={[5]} max={max} aria-label="Slider with ticks">
<SliderThumb />
</Slider>
<span
className="text-muted-foreground mt-3 flex w-full items-center justify-between gap-1.125 px-0.5 text-xs font-medium"
aria-hidden="true"
>
{ticks.map((_, i) => (
<span
key={i}
className="flex w-0 flex-col items-center justify-center gap-2"
>
<span
className={cn(
"bg-muted-foreground/70 h-1 w-px",
i % skipInterval !== 0 && "h-0.5"
)}
/>
<span className={cn(i % skipInterval !== 0 && "opacity-0")}>
{i}
</span>
</span>
))}
</span>
</div>
</div>
</div>
);
}Price Range with Inputs
Combine sliders with number inputs for precise control.
"use client";
import { SliderThumb, Label, Input } from "@tilt-legal/cubitt-components/primitives";
import { useState, useId } from "react";
import { Slider } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
const id = useId();
const [sliderValue, setSliderValue] = useState([200, 800]);
const [minInput, setMinInput] = useState("200");
const [maxInput, setMaxInput] = useState("800");
const minValue = 80;
const maxValue = 900;
const handleSliderChange = (value: number | readonly number[]) => {
const values = Array.isArray(value) ? value : [value];
setSliderValue(values);
setMinInput(values[0].toString());
setMaxInput(values[1].toString());
};
const validateMinInput = () => {
const numValue = parseFloat(minInput);
if (
!isNaN(numValue) &&
numValue >= minValue &&
numValue <= sliderValue[1]
) {
setSliderValue([numValue, sliderValue[1]]);
} else {
setMinInput(sliderValue[0].toString());
}
};
const validateMaxInput = () => {
const numValue = parseFloat(maxInput);
if (
!isNaN(numValue) &&
numValue <= maxValue &&
numValue >= sliderValue[0]
) {
setSliderValue([sliderValue[0], numValue]);
} else {
setMaxInput(sliderValue[1].toString());
}
};
return (
<div className="space-y-4 w-full max-w-sm">
<div className="flex flex-col gap-2.5">
<Label>Price Range</Label>
<Slider
value={sliderValue}
onValueChange={handleSliderChange}
min={minValue}
max={maxValue}
step={10}
>
<SliderThumb />
<SliderThumb />
</Slider>
</div>
<div className="flex items-center justify-between gap-4">
<div className="space-y-2.5">
<Label htmlFor={`${id}-min`}>Min Price</Label>
<Input
id={`${id}-min`}
type="number"
value={minInput}
onChange={(e) => setMinInput(e.target.value)}
onBlur={validateMinInput}
/>
</div>
<div className="space-y-2.5">
<Label htmlFor={`${id}-max`}>Max Price</Label>
<Input
id={`${id}-max`}
type="number"
value={maxInput}
onChange={(e) => setMaxInput(e.target.value)}
onBlur={validateMaxInput}
/>
</div>
</div>
</div>
);
}Rating Slider
Create custom rating interfaces with emoji indicators.
"use client";
import { SliderThumb, Label } from "@tilt-legal/cubitt-components/primitives";
import { useState } from "react";
import { Slider } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
const [value, setValue] = useState([3]);
const labels = ["Awful", "Poor", "Okay", "Good", "Amazing"];
return (
<div className="space-y-3 w-full max-w-xs">
<div className="flex items-center justify-between gap-2">
<Label className="leading-6">Rate your experience</Label>
<span className="text-sm font-medium">{labels[value[0] - 1]}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-2xl">😡</span>
<Slider
value={value}
onValueChange={(newValue) =>
setValue(Array.isArray(newValue) ? newValue : [newValue])
}
min={1}
max={5}
>
<SliderThumb />
</Slider>
<span className="text-2xl">😍</span>
</div>
</div>
);
}Climate Control
Complex example with multiple sliders and reset functionality.
"use client";
import {
SliderThumb,
Label,
Input,
Button,
} from "@tilt-legal/cubitt-components/primitives";
import { useState, useRef } from "react";
import { Slider } from "@tilt-legal/cubitt-components/primitives";
import {
Temperature,
Droplet,
Wind,
ArrowRotateClockwise,
} from "@tilt-legal/cubitt-icons/ui/outline";
function ClimateSlider({
minValue,
maxValue,
initialValue,
defaultValue,
label,
unit,
icon,
onRegisterReset,
}: {
minValue: number;
maxValue: number;
initialValue: number[];
defaultValue: number[];
label: string;
unit: string;
icon: React.ReactNode;
onRegisterReset: (resetFn: () => void) => void;
}) {
const [sliderValue,
setSliderValue] = useState(initialValue);
const [inputValue,
setInputValue] = useState(initialValue[0].toString());
// Register reset function
React.useEffect(() => {
onRegisterReset(() => {
setSliderValue(defaultValue);
setInputValue(defaultValue[0].toString());
});
},
[onRegisterReset,
defaultValue]);
// Handle changes...
return (
<div className="w-full space-y-0.5">
<div className="flex items-center gap-2">
<div className="text-muted-foreground">{icon}</div>
<Label className="text-sm font-medium">{label}</Label>
</div>
<div className="flex items-center gap-5">
<Slider
className="grow"
value={sliderValue}
onValueChange={(value) => {
const values = Array.isArray(value) ? value : [value];
setSliderValue(values);
setInputValue(values[0].toString());
}}
min={minValue}
max={maxValue}
step={0.5}
>
<SliderThumb />
</Slider>
<Input
className="h-8 w-16 px-2 py-1 text-center"
type="number"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
</div>
</div>
);
}
export default function Component() {
const resetFunctionsRef = useRef<(() => void)[]>([]);
return (
<div className="space-y-4 w-full max-w-sm">
<div className="space-y-3">
<ClimateSlider
minValue={16}
maxValue={30}
initialValue={[22]}
defaultValue={[22]}
label="Temperature"
unit="°C"
icon={<Temperature className="w-4 h-4" />}
onRegisterReset={(fn) => (resetFunctionsRef.current[0] = fn)}
/>
{/* More sliders... */}
</div>
<Button
className="w-full"
variant="secondary"
onClick={() => resetFunctionsRef.current.forEach((fn) => fn())}
>
<ArrowRotateClockwise size={16} />
Reset to Defaults
</Button>
</div>
);
}Vertical Slider
Sliders can be oriented vertically for space-constrained layouts.
import { Slider } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<div className="flex h-40 justify-center">
<Slider
defaultValue={[2, 7]}
max={10}
orientation="vertical"
aria-label="Vertical slider"
showTooltip
tooltipContent={(value) => `Value: ${value}`}
tooltipVariant="light"
/>
</div>
);
}Disabled
import { Slider, SliderThumb } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<div className="w-full max-w-xs flex flex-col gap-4">
<Slider defaultValue={[50]} disabled>
<SliderThumb />
</Slider>
<Slider defaultValue={[25, 75]} disabled>
<SliderThumb />
<SliderThumb />
</Slider>
</div>
);
}URL State
Sync the slider with the URL using paramName.
import { Slider, Label } from "@tilt-legal/cubitt-components/primitives";
export default function Component() {
return (
<div className="w-full max-w-sm flex flex-col gap-3">
<Label>Volume</Label>
<Slider
paramName="demo-volume"
defaultValue={[50]}
max={100}
step={5}
showTooltip
/>
<span
className="text-muted-foreground flex w-full items-center justify-between gap-2 text-xs font-medium"
aria-hidden="true"
>
<span>0%</span>
<span>100%</span>
</span>
</div>
);
}API Reference
Slider
The root slider component with track and range indicator.
| Prop | Type | Default | Description |
|---|---|---|---|
defaultValue | number[] | — | The default value when uncontrolled. |
value | number[] | — | The controlled value of the slider. |
onValueChange | (value: number | number[]) => void | — | Callback fired when the value changes. |
min | number | 0 | The minimum value of the slider. |
max | number | 100 | The maximum value of the slider. |
step | number | 1 | The stepping interval. |
orientation | "horizontal" | "vertical" | "horizontal" | The orientation of the slider. |
disabled | boolean | false | Whether the slider is disabled. |
showTooltip | boolean | false | Show tooltip with value while dragging. |
tooltipContent | (value: number) => React.ReactNode | — | Custom content for the tooltip. |
tooltipVariant | "default" | "light" | "default" | The visual variant of the tooltip. |
name | string | — | The name for form submission. |
className | string | — | Additional CSS classes for the slider. |
URL State Props
When provided with a paramName, the slider will sync its value with URL parameters via TanStack Router search params. The URL updates only when you release the thumb, not while dragging.
| Prop | Type | Default | Description |
|---|---|---|---|
paramName | string | — | URL parameter name for syncing state with URL. Enables URL state management. |
onUrlValueChange | (value: number[]) => void | — | Callback when URL parameter value changes. |
paramClearOnDefault | boolean | true | Remove URL param when value equals default. |
paramThrottle | number | — | Throttle URL updates in milliseconds. |
paramDebounce | number | — | Debounce URL updates in milliseconds. |
SliderThumb
The draggable thumb control for the slider.
| Prop | Type | Description |
|---|---|---|
className | string | Additional CSS classes for the thumb. |