Slider
A value chosen along a range. Pass an array and it becomes a range slider — there is no separate prop for that, because the shape of the value already says which one this is.
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 |
| disabled | boolean | false | Unavailable |
onValueChange fires throughout the drag; onValueCommitted fires once, when the value settles. Put the network request on the second one.
Examples
Range
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>
);
}Sizes
The thumb is deliberately far larger than the track. It is the only part of the control you can actually hit, and a thumb sized to match a 6px rail is a thumb nobody catches on a touchscreen.
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>
);
}Vertical
A vertical slider has no length of its own — give it a height. The default is a starting point, not a rule.
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>
);
}The thumb does not grow
Hovering and dragging put a ring around the thumb rather than scaling it. That is the same no-transform rule the rest of the library follows, and it is not relaxed just because this particular part carries no label: a control whose parts change size under the cursor is the thing that reads as cheap.
The rail and the indicator are pills for the same reason a Switch's track is — this is a groove something travels along, not a sheet.
Accessibility
- Each thumb is a real
<input type="range">, so the arrow keys, Home/End and Page Up/Down all work without any code here. labelbecomes the accessible name. Without one, give the slider anaria-label.showValuerenders an<output>, which is announced as the value changes.