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-js
import { 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
}: React.ComponentProps<typeof BaseSlider.Root>) {
const _values = React.useMemo(
() =>
Array.isArray(value)
? value
: Array.isArray(defaultValue)
? defaultValue
: [min, max],
[value, defaultValue, min, max],
);
return (
<BaseSlider.Root
defaultValue={defaultValue}
value={value}
min={min}
max={max}
{...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 px-2 inset-ring select-none',
className,
)}
>
<BaseSlider.Track className={cn('h-4 w-full select-none')}>
<BaseSlider.Indicator
className={cn(
'bg-blue-500 select-none',
'before:absolute before:top-0 before:bottom-0 before:left-0 before:w-2 before:-translate-x-full before:rounded-l-full before:bg-blue-500 before:content-[""]',
)}
/>
{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: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:bg-blue-500 before:content-[""]',
)}
/>
))}
</BaseSlider.Track>
</BaseSlider.Control>
</BaseSlider.Root>
);
}
interface ColorRangeSliderProps
extends Omit<
React.ComponentProps<typeof BaseSlider.Root>,
'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: { event: Event },
activeThumbIndex: number,
) => {
const nextNumber = Array.isArray(next) ? (next[0] ?? 0) : next;
if (!isControlled) setInternalValue(nextNumber);
onValueChange?.(nextNumber, eventDetails.event, 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}
{...props}
>
<BaseSlider.Control
className={cn(
'inset-ring-black-100 flex w-32 touch-none items-center rounded-full px-2 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: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(--thumb-color)]',
)}
style={
{
'--thumb-color': `hsl(${hue}, 100%, 50%)`,
} as React.CSSProperties
}
/>
</BaseSlider.Track>
</BaseSlider.Control>
</BaseSlider.Root>
);
}
interface OpacitySliderProps
extends Omit<
React.ComponentProps<typeof BaseSlider.Root>,
'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>
);
}