Slider
A slider component based on the native range input
import {
Slider,
SliderRange,
SliderThumb,
SliderTrack,
} from '@/components/slider';
export default function SliderPreview() {
return (
<Slider min={0} max={100} defaultValue={50}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb />
</Slider>
);
} Source Code
'use client';
import {
createContext,
use,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import { Slot } from '@/components/slot';
import { composeRefs } from '@/lib/compose-refs';
import { cn, cva } from '@/lib/utils/classnames';
interface SliderContextValue {
value: number;
setValue: (value: number) => void;
min: number;
max: number;
step: number;
orientation: 'horizontal' | 'vertical';
disabled: boolean;
onValueChange?: (value: number) => void;
progressFactor: number;
}
const SliderContext = createContext<SliderContextValue | null>(null);
const useSliderContext = () => {
const context = use(SliderContext);
if (!context) {
throw new Error('Slider components must be used within a Slider');
}
return context;
};
const sliderStyle = cva({
base: [
'[--track-thickness:--spacing(2)]',
'[--thumb-size:--spacing(4)]',
'relative inline-flex outline-none focus:outline-none',
],
variants: {
orientation: {
horizontal: 'h-(--track-thickness) w-56 items-center',
vertical: 'h-56 w-(--track-thickness) justify-center',
},
disabled: {
true: 'pointer-events-none opacity-60',
false: '',
},
},
defaultVariants: {
orientation: 'horizontal',
disabled: false,
},
});
interface SliderProps
extends Omit<
React.ComponentPropsWithRef<'div'>,
'onChange' | 'defaultValue'
> {
min?: number;
max?: number;
step?: number;
value?: number;
defaultValue?: number;
onValueChange?: (value: number) => void;
orientation?: 'horizontal' | 'vertical';
disabled?: boolean;
asChild?: boolean;
}
const Slider = ({
min = 0,
max = 100,
step = 1,
value: valueProp,
defaultValue = min,
onValueChange,
orientation = 'horizontal',
disabled = false,
className,
asChild,
children,
ref,
...props
}: SliderProps) => {
const [internalValue, setInternalValue] = useState(defaultValue);
const value = valueProp ?? internalValue;
const setValue = useCallback(
(newValue: number) => {
if (valueProp === undefined) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
},
[valueProp, onValueChange]
);
const progressFactor = useMemo(() => value / max, [value, max]);
const contextValue = useMemo(
() => ({
value,
setValue,
min,
max,
step,
orientation,
disabled,
onValueChange,
progressFactor,
}),
[
value,
setValue,
min,
max,
step,
orientation,
disabled,
onValueChange,
progressFactor,
]
);
const Comp = asChild ? Slot : 'div';
return (
<SliderContext value={contextValue}>
<Comp
ref={ref}
className={cn(sliderStyle({ orientation, disabled }), className)}
style={{
'--progress-track-factor': `${progressFactor}`,
}}
tabIndex={-1}
{...props}
>
{children}
</Comp>
</SliderContext>
);
};
const sliderTrackStyle = cva({
base: [
'pointer-events-none absolute rounded-full bg-foreground-secondary/40',
],
variants: {
orientation: {
horizontal:
'left-[calc(var(--thumb-size)/2)] h-(--track-thickness) w-[calc(100%-var(--thumb-size))]',
vertical:
'bottom-[calc(var(--thumb-size)/2)] h-[calc(100%-var(--thumb-size))] w-(--track-thickness)',
},
},
defaultVariants: {
orientation: 'horizontal',
},
});
interface SliderTrackProps extends React.ComponentPropsWithRef<'div'> {
asChild?: boolean;
}
const SliderTrack = ({
className,
asChild,
ref,
...props
}: SliderTrackProps) => {
const { orientation } = useSliderContext();
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
className={cn(sliderTrackStyle({ orientation }), className)}
tabIndex={-1}
{...props}
/>
);
};
const sliderRangeStyle = cva({
base: ['pointer-events-none absolute z-5 rounded-full bg-accent/90'],
variants: {
orientation: {
horizontal: 'left-0 h-(--track-thickness)',
vertical: 'bottom-0 w-(--track-thickness)',
},
},
defaultVariants: {
orientation: 'horizontal',
},
});
interface SliderRangeProps extends React.ComponentPropsWithRef<'div'> {
asChild?: boolean;
}
const SliderRange = ({
className,
asChild,
ref,
style,
...props
}: SliderRangeProps) => {
const { orientation } = useSliderContext();
const Comp = asChild ? Slot : 'div';
const rangeStyle = {
[orientation === 'horizontal' ? 'width' : 'height']:
`calc(var(--progress-track-factor) * 100%)`,
...style,
};
return (
<Comp
ref={ref}
className={cn(sliderRangeStyle({ orientation }), className)}
style={rangeStyle}
tabIndex={-1}
{...props}
/>
);
};
const sliderThumbStyle = cva({
base: [
'absolute z-10 cursor-pointer appearance-none rounded-full outline-none',
// Reset default styles
'[&::-webkit-slider-thumb]:appearance-none',
'[&::-moz-range-thumb]:border-none',
// Thumb styles
'[&::-webkit-slider-thumb]:transition',
'[&::-webkit-slider-thumb]:h-(--thumb-size) [&::-webkit-slider-thumb]:w-(--thumb-size) [&::-webkit-slider-thumb]:bg-foreground',
'[&::-webkit-slider-thumb]:z-10 [&::-webkit-slider-thumb]:rounded-full',
'[&::-moz-range-thumb]:transition',
'[&::-moz-range-thumb]:h-(--thumb-size) [&::-moz-range-thumb]:w-(--thumb-size) [&::-moz-range-thumb]:bg-foreground',
'[&::-moz-range-thumb]:rounded-full',
// Hover states
'[&::-webkit-slider-thumb:hover]:scale-130',
'[&::-moz-range-thumb:hover]:scale-130',
// Active states
'[&:active::-webkit-slider-thumb]:ring-4',
'[&:active::-webkit-slider-thumb]:ring-foreground/40',
'[&:active::-moz-range-thumb]:ring-4',
'[&:active::-moz-range-thumb]:ring-foreground/40',
// Focus states - apply to pseudo-elements only
'[&:focus-visible::-webkit-slider-thumb]:ring-4',
'[&:focus-visible::-webkit-slider-thumb]:ring-ring',
'[&:focus-visible::-moz-range-thumb]:ring-4',
'[&:focus-visible::-moz-range-thumb]:ring-ring',
],
variants: {
orientation: {
horizontal: [
'h-(--track-thickness) w-full',
'[&::-webkit-slider-thumb]:-mt-[calc(var(--thumb-size)-var(--track-thickness))/2]',
'[&::-moz-range-thumb]:-mt-[calc(var(--thumb-size)-var(--track-thickness))/2]',
'[&::-webkit-slider-runnable-track]:h-(--track-thickness)',
'[&::-moz-range-track]:h-(--track-thickness)',
],
vertical: [
'h-full w-(--track-thickness) [direction:rtl] [writing-mode:vertical-lr]',
'[&::-webkit-slider-thumb]:-ml-[calc(var(--thumb-size)-var(--track-thickness))/2]',
'[&::-moz-range-thumb]:-ml-[calc(var(--thumb-size)-var(--track-thickness))/2]',
'[&::-webkit-slider-runnable-track]:w-(--track-thickness)',
'[&::-moz-range-track]:w-(--track-thickness)',
],
},
},
defaultVariants: {
orientation: 'horizontal',
},
});
type SliderThumbProps = Omit<
React.ComponentPropsWithRef<'input'>,
'value' | 'type'
>;
const SliderThumb = ({
className,
onChange,
ref,
...props
}: SliderThumbProps) => {
const { value, setValue, min, max, step, orientation, disabled } =
useSliderContext();
const internalRef = useRef<HTMLInputElement>(null);
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = Number(e.target.value);
setValue(newValue);
// Update CSS variable for range visualization
const sliderElement = e.target.closest(
'[style*="--progress-track-factor"]'
) as HTMLElement;
if (sliderElement) {
sliderElement.style.setProperty(
'--progress-track-factor',
`${newValue / max}`
);
}
onChange?.(e);
},
[setValue, max, onChange]
);
return (
<input
ref={composeRefs(ref, internalRef)}
type="range"
min={min}
max={max}
step={step}
value={value}
disabled={disabled}
onChange={handleChange}
className={cn(sliderThumbStyle({ orientation }), className)}
{...props}
/>
);
};
export { Slider, SliderRange, SliderThumb, SliderTrack }; API Reference
A composable slider component built on the native range input.
Slider
The root container component.
| Prop | Default | Type | Description |
|---|---|---|---|
min | 0 | number | Minimum value of the slider. |
max | 100 | number | Maximum value of the slider. |
step | 1 | number | Step increment value. |
value | - | number | Controlled value of the slider. |
defaultValue | - | number | Default value for uncontrolled usage. |
onValueChange | - | (value: number) => void | Callback function when value changes. |
orientation | "horizontal" | "horizontal" | "vertical" | Orientation of the slider. |
disabled | false | boolean | Whether the slider is disabled. |
asChild | false | boolean | Render as child element using Slot. |
SliderTrack
The track background element.
| Prop | Default | Type | Description |
|---|---|---|---|
asChild | false | boolean | Render as child element using Slot. |
SliderRange
The filled range portion of the track.
| Prop | Default | Type | Description |
|---|---|---|---|
asChild | false | boolean | Render as child element using Slot. |
SliderThumb
The draggable thumb element (renders as input[type="range"]). Extends all standard input props except value and type.
The component uses CSS variables for customization:
--track-thickness- Thickness of the slider track--thumb-size- Size of the slider thumb
Examples
Simple
Basic usage with composable structure. The slider requires SliderTrack, SliderRange, and SliderThumb children.
<Slider min={0} max={100} defaultValue={50}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb />
</Slider>
import {
Slider,
SliderRange,
SliderThumb,
SliderTrack,
} from '@/components/slider';
export default function SliderPreview() {
return (
<Slider min={0} max={100} defaultValue={50}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb />
</Slider>
);
} Steps
Slider with defined step intervals.
import {
Slider,
SliderRange,
SliderThumb,
SliderTrack,
} from '@/components/slider';
export default function SliderPreview() {
return (
<Slider min={0} max={100} defaultValue={50} step={10}>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb />
</Slider>
);
} Vertical
Vertical orientation of the slider.
import {
Slider,
SliderRange,
SliderThumb,
SliderTrack,
} from '@/components/slider';
export default function SliderPreview() {
return (
<Slider min={0} max={100} defaultValue={50} orientation="vertical">
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb />
</Slider>
);
} Disabled
Disabled state of the slider.
import {
Slider,
SliderRange,
SliderThumb,
SliderTrack,
} from '@/components/slider';
export default function SliderPreview() {
return (
<Slider min={0} max={100} defaultValue={50} disabled>
<SliderTrack>
<SliderRange />
</SliderTrack>
<SliderThumb />
</Slider>
);
} Best Practices
-
Usage Guidelines:
- Always provide clear min and max values
- Consider step intervals for precise control
- Use appropriate slider length for the value range
- Set a meaningful default value
-
Accessibility:
- Provide clear visual feedback
- Include aria-labels for screen readers
- Ensure keyboard navigation works (arrow keys)
-
Interaction:
- Provide immediate visual feedback
- Consider adding value tooltips for precision
- Handle both mouse and touch interactions
Previous
Skeleton
Next
Spinner