Slider with tooltip
Free · MITSingle or range slider built from scratch with pointer and keyboard control, step snapping, marks and a value bubble that scales in while dragging.
Installation
$ npx shadcn@latest add https://goodui.dev/r/slider-tooltip.jsonThe file lands in components/goodui/slider-tooltip.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/slider-tooltip works too.
Usage
import * as React from "react"
import { SliderTooltip } from "@/components/goodui/slider-tooltip"
export function PriceFilter() {
const [price, setPrice] = React.useState([40, 120])
return (
<div className="grid gap-4 rounded-xl border bg-card p-5 pt-10">
<SliderTooltip
value={price}
onValueChange={setPrice}
onValueCommit={(v) => console.log("fetch services between", v)}
min={0}
max={200}
step={5}
minStepsBetweenThumbs={2}
formatValue={(v) => "$" + v}
thumbLabels={["Minimum price", "Maximum price"]}
marks={[{ value: 0 }, { value: 100 }, { value: 200 }]}
name="price"
/>
</div>
)
}When to use
- Price or duration filters where a range reads better than two number fields: "$40 to $120", "30 to 90 min".
- Picking a value on a known scale where the exact number matters less than its position: discount %, team size, volume.
- Settings with a few named stops, using marks: Basic, Pro, Team.
- Exact numbers people already know, such as a quantity or an amount: use input.
- Two to four named options with no scale in between: use segmented-control.
- Showing progress that the user does not control: use progress or meter.
| If you need | Use |
|---|---|
| One value | value={[n]} (a one-item array) |
| A range with two thumbs | value={[from, to]}, plus minStepsBetweenThumbs to keep them apart |
| Bubble only while interacting | tooltip="auto" (default) |
| Value always visible | tooltip="always" (two close bubbles merge into one) |
| No bubble, show the value elsewhere | tooltip="never" |
| Named stops under the track | marks={[{ value, label }]} |
Examples
Single value with steps
<SliderTooltip
aria-label="Session length"
defaultValue={[60]}
min={15}
max={120}
step={15}
showSteps
formatValue={(v) => v + " min"}
/>Plan picker with named marks
<SliderTooltip
aria-label="Seats"
defaultValue={[10]}
min={1}
max={50}
tooltip="always"
formatValue={(v) => v + " seats"}
marks={[{ value: 1, label: "Solo" }, { value: 10, label: "Team" }, { value: 50, label: "Business" }]}
/>API reference
| Prop | Type | Default |
|---|---|---|
| value One value for a single thumb, two for a range. | number[] | — |
| defaultValue Initial values when uncontrolled; defaults to [min]. | number[] | — |
| onValueChange Called on every change while dragging or pressing keys, with all thumb values. | (value: number[]) => void | — |
| onValueCommit Fires when a drag ends or a key press changes the value (not when the value stays the same, e.g. at a bound). | (value: number[]) => void | — |
| min | number | 0 |
| max | number | 100 |
| step | number | 1 |
| minStepsBetweenThumbs Minimum gap between range thumbs, in steps. | number | 0 |
| marks Labelled ticks under the track; a mark without a label shows formatValue(value). | SliderMark[] | — |
| showSteps Draw a dot at every step (only when there are 40 steps or fewer). | boolean | — |
| formatValue Formats the bubble, mark labels and aria-valuetext, e.g. (v) => `$${v}`. | (value: number) => string | (v) => String(v) |
| tooltip "auto" shows the bubble while dragging, hovering or keyboard-focused. | "auto" | "always" | "never" | "auto" |
| thumbLabels Accessible names per thumb, e.g. ["Minimum price", "Maximum price"]. | string[] | — |
| disabled | boolean | — |
| name Form field name; one hidden input per thumb. | string | — |
| labels Text for built-in controls and screen readers; pass translations here. | Partial<SliderTooltipLabels> | — |
Also accepts every <div> attribute.
Accessibility
| Arrow Right / Arrow Up | Increases the focused thumb by one step (Arrow Left increases under dir="rtl"). |
| Arrow Left / Arrow Down | Decreases the focused thumb by one step (Arrow Right decreases under dir="rtl"). |
| Page Up / Page Down | Moves by a tenth of the range (or one step, whichever is larger). |
| Home / End | Moves to min / max, or as far as the other thumb allows in a range. |
| Tab | Moves between thumbs; keyboard focus shows the value bubble in auto mode. |
- Each thumb is a focusable element with role="slider", aria-valuemin, aria-valuemax, aria-valuenow and aria-valuetext from formatValue.
- Range thumbs are named by thumbLabels, or labels.minimum / labels.maximum ("Minimum" and "Maximum") by default; a single thumb takes thumbLabels[0], the root aria-label or aria-labelledby, and falls back to labels.value ("Value").
- The root is role="group" and carries aria-label too.
- The value bubble and marks are aria-hidden; screen readers get the value from aria-valuetext.
- Clicking the track moves the nearest thumb and focuses it, so arrow keys continue from there.
Motion
- The bubble scales in from 0.5 and rises 6px on a spring (stiffness 600, damping 30), and leaves in 120 ms.
- The dragged thumb grows to 1.18 on a spring; the thumbs themselves follow the pointer directly, with no lag.
- Reduced motion: the bubble only fades (100 ms) and the dragged thumb does not grow.
- Dragging uses pointer capture on the track, so moves outside the slider keep working without window listeners.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- value and defaultValue are arrays: [n] for one thumb, [from, to] for a range. Passing a plain number is a type error.
- Use onValueChange for live UI and onValueCommit to fetch or save; commit fires when a drag ends or a key press actually changes the value (not at a bound).
- Built-in thumb names come from the labels prop (minimum, maximum, value); pass translations there.
- Thumbs cannot cross; minStepsBetweenThumbs is counted in steps, not in value units.
- With name, a range submits name[] twice (one per thumb); a single thumb submits name.
- Leave padding above the slider unless tooltip is "never": the bubble is positioned above the track and can overlap content there.
FAQ
How do I format the value as currency?
Pass formatValue, e.g. (v) => "$" + v or an Intl.NumberFormat. It is used for the bubble, unlabelled marks and aria-valuetext.
Does it support vertical sliders?
No. It is horizontal only (aria-orientation="horizontal").
Does it work right-to-left?
Yes. Under dir="rtl" the track fills from the right, marks and bubbles mirror, dragging maps from the right edge, and Arrow Left increases.
Related
Six field styles with top, floating, inline or hidden labels, focus ring, glow, underline-grow or lift, error shake with sliding messages, icon and text addons, clear button, counter and auto-growing textarea.
iOS-style segmented control whose thumb springs to the chosen segment and squeezes while pressed, with sizes, icons, full-width mode, dividers that hide next to the thumb, and radio-group arrow keys.
Labeled usage meter like "SMS credits 7,420 / 10,000" whose fill springs to the new value and shifts to amber and rose near the limit, with optional per-seat segments.
Linear progress in four variants: spring-filled determinate, indeterminate double sweep, moving stripes and segmented steps.