Slider
Picks a value by dragging along a range. Use it where the relative magnitude matters more than the exact number.
import { Slider } from 'neba';
<Slider label="Volume" defaultValue={65} showValue />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| orientationshared | 'horizontal' | 'vertical' | 'horizontal' | Which way the track runs. A vertical slider needs a height of its own |
| value | number | number[] | — | The current value. An array gives one thumb per entry |
| defaultValue | number | number[] | — | The initial value, for an uncontrolled slider |
| onValueChange | (value, details) => void | — | Called throughout the drag |
| onValueCommitted | (value, details) => void | — | Called once, when the value settles. Put the network request here |
| min | number | 0 | The lowest allowed value |
| max | number | 100 | The highest allowed value |
| step | number | 1 | The granularity the value moves in |
| label | ReactNode | — | The label above the track |
| description | ReactNode | — | Helper text below the track |
| showValue | boolean | ((formatted, values) => ReactNode) | false | Shows the current value beside the label. Pass a function to format it |
| marks | boolean | readonly SliderMark[] | false | Points named along the track, as an array of { value, label? }. true is a tick at every step |
| disabled | boolean | false | Unavailable |
| classNames | NebaSlots<'label' | 'control' | 'track' | 'indicator' | 'thumb' | 'description' | 'mark'> | — | Class names for the parts behind the root. The root itself is className, so there is no root key |
onValueChange fires throughout the drag; onValueCommitted fires once, when the value settles. Put the network request on the latter.
When an exact number has to be typed, use NumberField.
Examples
An array value makes it a range
Pass an array of numbers as the value and you get that many thumbs: a range slider. There is no separate prop for it.
import { useState } from 'react';
import { Slider } from 'neba';
export default function SliderRange() {
const [range, setRange] = useState<number[]>([200, 800]);
return (
<div className="w-full max-w-md">
<Slider
label="Price"
min={0}
max={1000}
step={50}
value={range}
onValueChange={(next) => setRange(next as number[])}
showValue={(formatted) => `$${formatted[0]} – $${formatted[1]}`}
/>
</div>
);
}min · max · step
step is the interval the thumb settles on. showValue prints the current value beside the label.
marks
marks names points along the track: 1 / 100 / 250 / 500 under a count, or the two ends of a style axis. Pass an array of { value, label? }, and a mark with no label is a tick on its own.
marks without a value is a tick at every step, which is worth pairing with a step you chose — the default step={1} over the default range would be a hundred of them, so that case draws none at all.
The row is hidden from screen readers: the thumb already announces the value and the range.
import { Slider } from 'neba';
const SIZES = [
{ value: 1, label: '1' },
{ value: 100, label: '100' },
{ value: 250, label: '250' },
{ value: 500, label: '500' }
];
const STYLES = [
{ value: 0, label: 'Realistic' },
{ value: 100, label: 'Abstract' }
];
export default function SliderMarks() {
return (
<div className="flex w-full max-w-sm flex-col gap-8">
<Slider label="Batch size" min={1} max={500} defaultValue={100} marks={SIZES} showValue />
<Slider label="Style" defaultValue={50} marks={STYLES} />
{/* `marks` on its own is a tick at every step, which is worth pairing
with a step you chose. */}
<Slider label="Strength" step={20} defaultValue={40} marks showValue />
</div>
);
}size
The thumb is drawn larger than the track: it is the part you actually hit, so it needs a real touch target.
import { Slider } from 'neba';
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
export default function SliderSizes() {
return (
<div className="flex w-full max-w-md flex-col gap-6">
{SIZES.map((size) => (
<Slider key={size} size={size} label={size} defaultValue={50} />
))}
</div>
);
}orientation
A vertical slider has no length of its own; give it a height.
import { Slider } from 'neba';
const CHANNELS = [
{ label: 'Low', value: 70, color: 'primary' as const },
{ label: 'Mid', value: 45, color: 'info' as const },
{ label: 'High', value: 30, color: 'success' as const }
];
export default function SliderVertical() {
return (
<div className="flex items-end gap-8">
{CHANNELS.map((channel) => (
<div key={channel.label} className="flex flex-col items-center gap-2">
<Slider
orientation="vertical"
color={channel.color}
aria-label={channel.label}
defaultValue={channel.value}
/>
<span className="text-[0.75rem] text-[var(--neba-muted-fg)]">{channel.label}</span>
</div>
))}
</div>
);
}classNames
className lands on the root — the column holding the label, the strip and the line under it — and the parts inside are reached through classNames.
<Slider label="Volume" classNames={{ track: 'h-1', thumb: 'rounded-sm', mark: 'font-mono' }} />The slots are label, control, track, indicator, thumb, description and mark. control is the whole strip a press lands on, which is taller than the track drawn inside it.
Accessibility
- Each thumb is a real
<input type="range">, so the arrow keys, Home/End and Page Up/Down all work as they should. labelbecomes the accessible name. Without one, give the slider anaria-label.showValuerenders an<output>, which is announced as the value changes.- Hovering and dragging draw a ring around the thumb rather than changing its size.