Slider with tooltip

Free · MIT

Single or range slider built from scratch with pointer and keyboard control, step snapping, marks and a value bubble that scales in while dragging.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/slider-tooltip.json

The 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

Use it for
  • 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.
Reach for something else
  • 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 needUse
One valuevalue={[n]} (a one-item array)
A range with two thumbsvalue={[from, to]}, plus minStepsBetweenThumbs to keep them apart
Bubble only while interactingtooltip="auto" (default)
Value always visibletooltip="always" (two close bubbles merge into one)
No bubble, show the value elsewheretooltip="never"
Named stops under the trackmarks={[{ 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

PropTypeDefault
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—
minnumber0
maxnumber100
stepnumber1
minStepsBetweenThumbs
Minimum gap between range thumbs, in steps.
number0
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[]—
disabledboolean—
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 UpIncreases the focused thumb by one step (Arrow Left increases under dir="rtl").
Arrow Left / Arrow DownDecreases the focused thumb by one step (Arrow Right decreases under dir="rtl").
Page Up / Page DownMoves by a tenth of the range (or one step, whichever is larger).
Home / EndMoves to min / max, or as far as the other thumb allows in a range.
TabMoves 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