Browse docs
Browse docs
A numeric value picker on a horizontal track. Drag the thumb (or use the keyboard) to choose a number, or switch to range mode for a [min, max] tuple. Built on @radix-ui/react-slider, so keyboard control, pointer capture, and RTL come for free.
Typical uses: volume / opacity / brightness (single), price or date-window filters (range).
import { Slider } from '@dashforge/tw';
<Slider name="volume" label="Volume" min={0} max={100} defaultValue={40} />import { DashForm } from '@dashforge/forms';
import { Slider, Button } from '@dashforge/tw';
<DashForm onSubmit={onSubmit}>
<Slider name="volume" label="Volume" min={0} max={100} showValueLabel="auto" />
<Button type="submit" color="primary">Save</Button>
</DashForm>Inside a <DashForm> the field self-registers with the bridge. By default it commits on drag-end (and on every keyboard step) — see Commit strategy. Outside a form, drive it controlled with value + onChange, or uncontrolled with defaultValue.
import { useState } from 'react';
import { Slider } from '@dashforge/tw';
const [volume, setVolume] = useState(40);
<Slider
name="volume"
label="Volume"
min={0}
max={100}
value={volume}
onChange={setVolume}
showValueLabel="auto"
/>const [volume, setVolume] = useState(40);
<Slider
name="volume"
label="Volume"
min={0}
max={100}
value={volume}
onChange={setVolume}
showValueLabel="auto"
/>Controlled: onChange fires on every drag tick and keyboard step, so value tracks the thumb live. showValueLabel="auto" floats the current value above the thumb on hover, drag, or focus.
import { useState } from 'react';
import { Slider } from '@dashforge/tw';
const [price, setPrice] = useState<readonly [number, number]>([200, 800]);
<Slider
name="price"
label="Price range"
range
min={0}
max={1000}
step={50}
value={price}
onChange={setPrice}
showValueLabel="auto"
formatValue={(v) => `${v}`}
/>const [price, setPrice] = useState<readonly [number, number]>([200, 800]);
<Slider
name="price"
label="Price range"
range
min={0}
max={1000}
step={50}
value={price}
onChange={setPrice}
showValueLabel="auto"
formatValue={(v) => `${v}`}
/>range turns value / defaultValue into a [number, number] tuple and renders two thumbs. The highlighted segment spans between them. formatValue styles the value-label (and auto-mark labels).
import { useState } from 'react';
import { Slider } from '@dashforge/tw';
const MARKS = [
{ value: 0, label: 'Off' },
{ value: 25, label: 'Low' },
{ value: 50, label: 'Mid' },
{ value: 75, label: 'High' },
{ value: 100, label: 'Max' },
];
const [brightness, setBrightness] = useState(50);
<Slider
name="brightness"
label="Brightness"
min={0}
max={100}
step={25}
marks={MARKS}
value={brightness}
onChange={setBrightness}
formatValue={(v) => `${v}%`}
/>const MARKS = [
{ value: 0, label: 'Off' },
{ value: 25, label: 'Low' },
{ value: 50, label: 'Mid' },
{ value: 75, label: 'High' },
{ value: 100, label: 'Max' },
];
<Slider name="brightness" label="Brightness" min={0} max={100} step={25} marks={MARKS} />marks accepts an explicit { value, label? }[], or true to auto-generate a tick at every step (suppressed above 21 ticks to avoid cramming the track). Marks between the selected thumbs flip to the in-range colour.
<Slider name="a" color="primary" defaultValue={40} /> {/* default */}
<Slider name="b" color="success" defaultValue={60} />
<Slider name="c" color="warning" defaultValue={70} />
<Slider name="d" color="danger" defaultValue={80} />
<Slider name="e" color="neutral" defaultValue={50} />Six intents — primary, secondary, success, warning, danger, neutral — tint the range segment, thumb, and value-label. In error state the focus ring turns danger regardless of the chosen intent.
<Slider size="sm" name="x" defaultValue={40} />
<Slider size="md" name="x" defaultValue={40} /> {/* default */}
<Slider size="lg" name="x" defaultValue={40} />
<Slider layout="inline" name="x" label="Zoom" defaultValue={40} />Two callbacks, two moments:
| Callback | Fires | Writes to the form bridge? |
|---|---|---|
onChange | Every drag tick + keyboard step | No (UI-only) — unless commitOnChange |
onCommit | Drag-end (pointer up) + every keyboard step | Yes |
By default the bridge is written on drag-end (matching MUI / Ant) so a drag across the track is one form update, not sixty. Set commitOnChange when the form must react live during the drag (e.g. a price filter feeding a product list):
<Slider name="priceRange" range commitOnChange min={0} max={1000} />Keyboard changes always commit immediately — each arrow-key press is a single tick and a single commit, regardless of commitOnChange.
Configure <Slider> defaults application-wide via Option C.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
Slider: {
defaults: { size: 'md', layout: 'stacked', color: 'primary', fullWidth: true },
},
},
});Configurable axes (SliderVariantProps):
| Axis | Type | Notes |
|---|---|---|
size | 'sm' | 'md' | 'lg' | Track height + thumb size + value-label font. |
layout | 'stacked' | 'inline' | Label position (above vs left of the control). |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'neutral' | Range segment + thumb + value-label tint. |
fullWidth | boolean | Stretch the root to container width. |
Non-visual axes (name, rules, range, value, defaultValue, onChange, onCommit, min, max, step, marks, showValueLabel, formatValue, commitOnChange, error, required, disabled, access, visibleWhen) are not theme-configurable — per-instance semantics.
Precedence chain (lowest → highest):
defaultVariants from the internal sliderVariants recipe.theme.components.Slider.defaults (application-wide).<Slider color="success" size="lg" />) — wins over theme.sx — appended to the control wrapper and wins via tailwind-merge.Reactivity: useComponentDefaults('Slider') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
The table below lists the full prop surface in single-value mode (SliderSingleProps). Range mode differs only in the value-carrying props — see Range mode below.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Field name. Required — matches every other tw form widget. Enforced at the type level to prevent the "silent no-op" family of bugs (see #113). |
access | AccessRequirement | — | RBAC gate — see useAccessState. |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'neutral' | 'primary' | Colour intent for the range segment + thumb + value label. neutral auto-inverts via the preset CSS-var swap. Configurable application-wide via theme.components.Slider.defaults.color. |
commitOnChange | boolean | false | When true, writes to the form bridge on every drag tick instead of only on drag-end. Costs 60 setValue/sec during drag but gives the "value in form == value shown" semantic if the consumer's form depends on the value reactively (e.g. price-range filter feeding a live product list). Keyboard changes always commit immediately regardless of this setting — each arrow-key press is a single tick + commit. |
defaultValue | number | — | Uncontrolled initial value. |
disabled | boolean | — | Grays out the control; blocks drag + keyboard. |
error | boolean | — | Force error state (danger colour on track + thumb, aria-invalid on the thumb). |
formatValue | (value: number) => ReactNode | (value) => String(value) | Format the display value in the value label tooltip and in the marks' labels when marks: true auto-labelling kicks in. |
fullWidth | boolean | — | Stretches the root to w-full. |
helperText | ReactNode | — | Descriptive line below the control. Replaced by the validation error message when the field is invalid. |
label | ReactNode | — | Text or node above (or to the left in inline layout) the control. |
layout | 'stacked' | 'inline' | 'stacked' | Layout — stacked (label above) or inline (label left of the control). Matches TextField / Select / Autocomplete. |
marks | readonly SliderMark[] | boolean | — | Tick marks on the track. - readonly SliderMark[] — explicit marks with optional labels. - true — auto-generate marks at every step position (only up to a reasonable density; skipped if the resulting count > 21). - false / omitted — no marks. |
max | number | 100 | Upper bound of the track. |
min | number | 0 | Lower bound of the track. |
onBlur | () => void | — | Fires when the thumb loses focus (after a commit). |
onChange | (value: number) => void | — | Fires on every drag tick + keyboard step. UI-side handler — does NOT write to the bridge unless commitOnChange is true. |
onCommit | (value: number) => void | — | Fires on drag-end (pointer up) and on every keyboard step. This is what writes to the form bridge — matches MUI / Ant defaults. |
range | false | false | Discriminator — omit or set false for single-value mode. Selects the number value/callback shapes over the range tuple. |
required | boolean | — | Renders the red * next to the label. |
rules | unknown | — | RHF rules. Forwarded to bridge.register(name, rules) inside a <DashFormProvider>. Ignored standalone. |
showValueLabel | 'auto' | 'always' | 'off' | 'off' | Show the current value as a tooltip above the thumb. - 'auto' — visible on hover, drag, or keyboard focus. - 'always' — always visible. - 'off' — never rendered. |
size | 'md' | 'sm' | 'lg' | 'md' | Density knob. |
slotProps | SliderSlotProps | — | Per-slot overrides. |
step | number | 1 | Step size between valid values. keyboardStep in Radix defaults to this. Set to 1 for integers, 0.1 / 0.01 for fractional. |
sx | string | — | Utility-class shortcut for slotProps.controlWrapper.className — mirrors the Dashforge idiom on other primitives. |
testId | string | — | data-testid on the root wrapper. |
value | number | — | Controlled value. |
visibleWhen | (engine: Engine) => boolean | — | Reactive visibility predicate. Falsy → renders null. |
SliderProps is a discriminated union on range. Passing range={true} swaps the value-carrying props to the tuple shape (SliderRangeProps):
| Prop | Single (range omitted / false) | Range (range: true) |
|---|---|---|
range | false (default) | true |
value | number | readonly [number, number] |
defaultValue | number | readonly [number, number] |
onChange | (value: number) => void | (value: readonly [number, number]) => void |
onCommit | (value: number) => void | (value: readonly [number, number]) => void |
All other props (name, min, max, step, marks, color, size, …) are identical across both modes. TypeScript narrows the value/callback types automatically from the runtime range prop.
<Slider> is a compound component. Each slot accepts a { className?: string } override via slotProps:
| Slot | Purpose |
|---|---|
root | Outer wrapper (label + control + helper/error). |
label | The <label> above (or left of) the control. |
requiredMark | The red * next to the label when required. |
controlWrapper | Flex container holding the Radix Slider.Root. |
track | The full horizontal bar. |
rangeSegment | The highlighted portion (start→thumb, or thumb→thumb in range). |
thumb | The draggable handle (one per value). |
mark | A tick on the track for a discrete step. |
markLabel | The text label under a mark. |
valueLabel | The tooltip above the thumb showing the current value. |
helperText | Helper text line, when not in error state. |
errorText | Helper text line, when in error state. |
Use sx for a control-wrapper class override; use slotProps to reach a specific inner element (e.g. track height, thumb colour, valueLabel background).
onChange is per-tick UI; onCommit (drag-end + keyboard) is what writes the bridge. Opt into per-tick bridge writes with commitOnChange. See above.←/→ (and ↑/↓) step by step, Home/End jump to min/max, PageUp/PageDown take larger steps — all from Radix, and all commit immediately.true auto-generates a tick per step but bails out above 21 ticks to avoid a crammed track; pass an explicit array for full control over which ticks and labels appear.showValueLabel="auto" shows the tooltip on hover / drag / focus; "always" pins it; "off" (default) hides it. formatValue styles both the tooltip and auto-mark labels.role="slider", aria-valuenow/min/max, and a descriptive aria-label (in range mode, "… minimum" / "… maximum"). aria-invalid is set in error state.value / onChange use readonly [number, number] — type your state as readonly [number, number] (or let useState infer it) to avoid a tuple-mutability mismatch.