Use Slider for an approximate numeric setting with a meaningful range. This wrapper takes arrays even for one thumb. For exact amounts, provide a synchronized numeric input in your application.
import { Slider } from "@/components/ui/slider";
export default function SliderDemo() {
return <Slider defaultValue={[33]} getAriaLabel={() => "Value"} max={100} step={1} />;
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/slider.jsonnpm install @base-ui/react@^1.8.0Create components/ui/slider.tsx:
"use client";
import { DirectionProvider } from "@base-ui/react/direction-provider";
import { Slider as SliderPrimitive } from "@base-ui/react/slider";
import { cn } from "@/lib/utils";
type SliderProps = SliderPrimitive.Root.Props<number[]> & {
dir?: "ltr" | "rtl";
getAriaLabel?: (index: number) => string;
};
function Slider({
className,
defaultValue,
dir,
getAriaLabel,
min = 0,
orientation = "horizontal",
thumbCollisionBehavior = "swap",
value,
...props
}: SliderProps) {
const initialValues = defaultValue ?? [min];
const values = value ?? initialValues;
const slider = (
<SliderPrimitive.Root<number[]>
data-slot="slider"
className={(state) =>
cn(
"w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-auto",
typeof className === "function" ? className(state) : className,
)
}
defaultValue={initialValues}
value={value}
min={min}
orientation={orientation}
thumbCollisionBehavior={thumbCollisionBehavior}
{...props}
>
<SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col">
<SliderPrimitive.Track
data-slot="slider-track"
className="relative h-3 w-full grow overflow-hidden rounded-base border-2 border-border bg-secondary-background select-none data-[orientation=vertical]:h-full data-[orientation=vertical]:w-3"
>
<SliderPrimitive.Indicator
data-slot="slider-range"
className="bg-main select-none data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
/>
</SliderPrimitive.Track>
{values.map((_, index) => (
<SliderPrimitive.Thumb
key={index}
data-slot="slider-thumb"
index={index}
aria-label={getAriaLabel?.(index)}
className="relative block size-5 shrink-0 rounded-full border-2 border-border bg-white transition-colors select-none after:absolute after:-inset-2 has-focus-visible:outline-none has-focus-visible:ring-1 has-focus-visible:ring-ring data-disabled:pointer-events-none data-disabled:opacity-50"
/>
))}
</SliderPrimitive.Control>
</SliderPrimitive.Root>
);
return dir ? <DirectionProvider direction={dir}>{slider}</DirectionProvider> : slider;
}
export { Slider };
#Usage
"use client";
import { useState } from "react";
import { Slider } from "@/components/ui/slider";
export default function VolumeSetting() {
const [volume, setVolume] = useState([50]);
return (
<div className="grid max-w-sm gap-3">
<p>Volume: {volume[0]}%</p>
<Slider
value={volume}
onValueChange={setVolume}
min={0}
max={100}
step={5}
getAriaLabel={() => "Volume"}
/>
</div>
);
}#API reference
| Prop | Purpose | Default |
|---|---|---|
value / onValueChange | Controlled numeric array | Uncontrolled |
defaultValue | Initial thumb values | [min] |
min / max / step | Range and step | 0 / 100 / 1 |
orientation | Horizontal or vertical layout | horizontal |
getAriaLabel | Name for each thumb index | Unset |
disabled | Prevent adjustment | false |
minStepsBetweenValues | Minimum thumb separation in steps | 0 |
thumbCollisionBehavior | swap, push, or none | swap |
onValueCommitted | Work performed when an interaction commits | Unset |
className | String or function of the Base UI slider state | — |
Base UI owns pointer capture, keyboard stepping, value normalization, collision behavior, form integration, and commit events. The wrapper only composes its styled parts and labels; it does not simulate native events or maintain a second slider engine.
#Accessibility
Name each thumb, especially for minimum/maximum ranges. Connect a visible group title with aria-labelledby and name each thumb with getAriaLabel. Show values and units in text. Test arrows, Home, and End, and keep pointer targets usable. Run expensive work at the end of interaction rather than on every drag update.
#Examples
#Two thumbs
"use client";
import { Slider } from "@/components/ui/slider";
export default function TwoThumbsSliderDemo() {
return (
<Slider
defaultValue={[25, 50]}
getAriaLabel={(index) => (index === 0 ? "Minimum value" : "Maximum value")}
max={100}
step={1}
/>
);
}
#Vertical
"use client";
import { Slider } from "@/components/ui/slider";
export default function VerticalSliderDemo() {
return (
<Slider
defaultValue={[25]}
getAriaLabel={() => "Volume"}
max={100}
step={1}
orientation="vertical"
/>
);
}
#Controlled
The visible title names the slider group, and each thumb has its own minimum or maximum label.
"use client";
import * as React from "react";
import { Slider } from "@/components/ui/slider";
export default function SliderControlled() {
const labelId = React.useId();
const [value, setValue] = React.useState([0.3, 0.7]);
return (
<div className="grid w-full gap-3">
<div className="flex items-center justify-between gap-2">
<span id={labelId} className="text-sm font-heading leading-none select-none">
Temperature
</span>
<span className="text-foreground font-base text-sm">{value.join(", ")}</span>
</div>
<Slider
aria-labelledby={labelId}
getAriaLabel={(index) => (index === 0 ? "Minimum temperature" : "Maximum temperature")}
value={value}
onValueChange={setValue}
min={0}
max={1}
step={0.1}
/>
</div>
);
}