Components
Slider
Slider is a component that allows the user to select a value from a range of values.
0
25, 45
'use client';
import React from 'react';
import { Slider } from '../ui/slider';
export default function SliderDemo() {
const [value, setValue] = React.useState(0);
const [value2, setValue2] = React.useState([25, 45]);
return (
<div className='flex flex-col items-center gap-4'>
<Slider
className='w-64'
value={value}
onValueChange={(value) => setValue(value as number)}
/>
<p>{value}</p>
<Slider
className='w-64'
value={value2}
onValueChange={(value) => setValue2(value as number[])}
/>
<p>{value2.join(', ')}</p>
</div>
);
}
Manual Installation
you need to install chroma-js to use the color range slider:
pnpm add chroma-jsimport { cn } from '@/lib/utils';
import { Slider as BaseSlider } from '@base-ui-components/react';
import React from 'react';
import chroma from 'chroma-js';
function Slider({
className,
value,
defaultValue,
min,
max,
...props
}: BaseSlider.Root.Props) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: typeof value === 'number'
? [value]
: Array.isArray(defaultValue)
? defaultValue
: typeof defaultValue === 'number'
? [defaultValue]
: [min ?? 0],
[value, defaultValue, min],
);
return (
<BaseSlider.Root
defaultValue={defaultValue}
value={value}
min={min}
max={max}
thumbAlignment='edge'
{...props}
>
<BaseSlider.Control
className={cn(
'bg-grey-100 dark:bg-grey-700 dark:inset-ring-grey-600 inset-ring-grey-200 flex w-32 touch-none items-center rounded-full inset-ring select-none',
className,
)}
>
<BaseSlider.Track className={cn('h-4 w-full select-none')}>
<BaseSlider.Indicator
className={cn(
'bg-blue-500 select-none',
_values.length === 1 ? 'rounded-l-full' : '',
)}
/>
{Array.from({ length: _values.length }).map((_, index) => (
<BaseSlider.Thumb
key={index}
className={cn(
'shadow-100 bg-white-1000 size-4 rounded-full select-none before:hidden',
)}
>
<span className='border-black-100 absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full border bg-blue-500' />
</BaseSlider.Thumb>
))}
</BaseSlider.Track>
</BaseSlider.Control>
</BaseSlider.Root>
);
}
interface ColorRangeSliderProps
extends Omit<
BaseSlider.Root.Props,
'value' | 'defaultValue' | 'min' | 'max' | 'onValueChange' | 'children'
> {
value?: number;
defaultValue?: number;
min?: number;
max?: number;
step?: number;
onValueChange?: (
value: number,
event: Event,
activeThumbIndex: number,
) => void;
}
function ColorRangeSlider({
className,
value,
defaultValue = 0,
min = 0,
max = 360,
step = 1,
onValueChange,
...props
}: ColorRangeSliderProps) {
const isControlled = value !== undefined;
const [internalValue, setInternalValue] = React.useState<number>(
defaultValue ?? 0,
);
React.useEffect(() => {
if (!isControlled && defaultValue !== undefined) {
setInternalValue(defaultValue);
}
}, [defaultValue, isControlled]);
const handleValueChange = React.useCallback(
(
next: number | number[],
eventDetails: BaseSlider.Root.ChangeEventDetails,
) => {
const nextNumber = Array.isArray(next) ? (next[0] ?? 0) : next;
if (!isControlled) setInternalValue(nextNumber);
onValueChange?.(
nextNumber,
eventDetails.event,
eventDetails.activeThumbIndex,
);
},
[isControlled, onValueChange],
);
const hue = isControlled ? (value as number) : internalValue;
return (
<BaseSlider.Root
value={value}
defaultValue={defaultValue}
min={min}
max={max}
step={step}
onValueChange={handleValueChange}
thumbAlignment='edge'
{...props}
>
<BaseSlider.Control
className={cn(
'inset-ring-black-100 flex w-32 touch-none items-center rounded-full inset-ring select-none',
'[background-image:linear-gradient(to_right,#FF0000_0%,#FFA800_13%,#FFFF00_22%,#00FF00_34%,#00FFFF_50%,#0000FF_66%,#FF00FF_82%,#FF0000_100%)]',
className,
)}
>
<BaseSlider.Track
className={cn('relative h-4 w-full rounded-full select-none')}
>
<BaseSlider.Thumb
className={cn(
'shadow-100 bg-white-1000 size-4 rounded-full select-none',
'before:hidden',
)}
>
<span
className='border-black-100 absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full border bg-blue-500'
style={{ backgroundColor: `hsl(${hue}, 100%, 50%)` }}
/>
</BaseSlider.Thumb>
</BaseSlider.Track>
</BaseSlider.Control>
</BaseSlider.Root>
);
}
interface OpacitySliderProps
extends Omit<
BaseSlider.Root.Props,
'value' | 'defaultValue' | 'min' | 'max' | 'onValueChange' | 'children'
> {
value?: number;
defaultValue?: number;
min?: number;
max?: number;
step?: number;
color?: string;
onValueChange?: (value: number) => void;
}
function OpacitySlider({
className,
value,
defaultValue = 0,
min = 0,
max = 1,
step = 0.01,
onValueChange,
color = 'black',
...props
}: OpacitySliderProps) {
const [lastValidHex, setLastValidHex] = React.useState<string>(() =>
chroma.valid(color) ? chroma(color).hex() : chroma('black').hex(),
);
React.useEffect(() => {
if (chroma.valid(color)) {
setLastValidHex(chroma(color).hex());
}
}, [color]);
const handleChange = React.useCallback(
(next: number | number[]) => {
const nextNumber = Array.isArray(next) ? (next[0] ?? 0) : next;
onValueChange?.(nextNumber);
},
[onValueChange],
);
return (
<BaseSlider.Root
value={value}
defaultValue={defaultValue}
min={min}
max={max}
step={step}
onValueChange={handleChange}
{...props}
>
<BaseSlider.Control
className={cn(
'inset-ring-black-100 flex w-32 touch-none items-center overflow-hidden rounded-full px-2 inset-ring select-none',
'[background-image:linear-gradient(to_right,transparent_0%,var(--opacity-color)_100%),conic-gradient(#d1d5db_25%,#0000_0_50%,#d1d5db_0_75%,#0000_0)]',
'[background-size:100%_100%,12px_12px]',
className,
)}
style={{ '--opacity-color': lastValidHex } as React.CSSProperties}
>
<BaseSlider.Track
className={cn('relative h-4 w-full rounded-full select-none')}
>
<BaseSlider.Thumb
className={cn(
'shadow-100 bg-white-1000 size-4 rounded-full select-none',
'before:border-black-100 before:absolute before:top-1/2 before:right-1/2 before:size-2 before:translate-x-1/2 before:-translate-y-1/2 before:rounded-full before:border before:content-[""]',
'before:[background-color:var(--opacity-color)]',
)}
style={
{
'--opacity-color': lastValidHex,
} as React.CSSProperties
}
/>
</BaseSlider.Track>
</BaseSlider.Control>
</BaseSlider.Root>
);
}
export { ColorRangeSlider, Slider, OpacitySlider };
Example
Up to now, we have three types of sliders:
- Default Slider
- Color Range Slider
- Opacity Slider
Color Range Slider
Color range slider is a slider that allows the user to select a color (hue value of HSL color space, more specifically) from a range of colors. The slider value is a number between 0 and 360.
Hue: 180°
hsl(180, 100%, 50%)
'use client';
import { ColorRangeSlider } from '@/components/ui/slider';
import { useState } from 'react';
export default function ColorRangeSliderDemo() {
const [hue, setHue] = useState(180);
// Convert hue to HSL color
const selectedColor = `hsl(${hue}, 100%, 50%)`;
return (
<div className='flex flex-col gap-6'>
<ColorRangeSlider
value={hue}
onValueChange={(value) => setHue(value as number)}
className={'w-56'}
/>
<div className='flex items-center gap-3'>
<div
className='h-12 w-12 rounded-lg border border-gray-200 dark:border-gray-700'
style={{ backgroundColor: selectedColor }}
/>
<div className='text-sm'>
<div className='font-medium text-gray-900 dark:text-gray-100'>
Hue: {hue}°
</div>
<div className='text-gray-500 dark:text-gray-400'>
{selectedColor}
</div>
</div>
</div>
</div>
);
}
Opacity Slider
Opacity slider is a slider that allows the user to select a value between 0 and 1. And accept a color string as a prop.
'use client';
import { Input } from '@/components/ui/input';
import { OpacitySlider } from '@/components/ui/slider';
import { useState } from 'react';
export default function OpacitySliderDemo() {
const [opacity, setOpacity] = useState(0.5);
const [color, setColor] = useState('#FF0505');
return (
<div className='flex flex-col gap-6'>
<Input value={color} onChange={(e) => setColor(e.target.value)} />
<OpacitySlider
value={opacity}
onValueChange={setOpacity}
color={color}
className='w-56'
/>
<span className='text-sm text-gray-500'>
opacity: {opacity.toFixed(2)}
</span>
</div>
);
}