A single or range slider with a name per thumb, a live value bubble, vertical orientation and full keyboard control, built on Radix.
Volume
Price range
"use client";
import * as React from "react";
import { Slider } from "@/components/ballmac/slider";
export default function SliderDemo() {
const [volume, setVolume] = React.useState([64]);
return (
<div className="grid w-full max-w-sm gap-6 rounded-xl border bg-card p-5 pt-8 shadow-sm">
<div className="grid gap-4">
<p id="vol-label" className="text-sm font-medium">
Volume
</p>
<Slider
aria-labelledby="vol-label"
value={volume}
onValueChange={setVolume}
showValue
formatValue={(v) => `${v}%`}
/>
</div>
<div className="grid gap-4">
<p id="price-label" className="text-sm font-medium">
Price range
</p>
<Slider
defaultValue={[20, 75]}
min={0}
max={200}
step={5}
thumbLabels={["Minimum price", "Maximum price"]}
showValue
formatValue={(v) => `$${v}`}
/>
</div>
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/sliderInstall the dependencies.
$ pnpm add radix-uiAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/directionCopy the source into your project.
components/ballmac/slider.tsx// Ballmac UI: Slider. https://ui.ballmac.com/components/slider // Based on shadcn/ui Slider (MIT, Copyright (c) 2023 shadcn), adding per-thumb labels, a live value bubble, and vertical support. "use client"; import * as React from "react"; import { Slider as Primitive } from "radix-ui"; import { useDirection } from "@/lib/ballmac/direction"; import { cn } from "@/lib/utils"; type SliderProps = Omit< React.ComponentProps<typeof Primitive.Root>, "value" | "defaultValue" > & { /** Controlled value. One number per thumb. */ value?: number[]; /** Initial value when uncontrolled. One number per thumb; two numbers make a range. */ defaultValue?: number[]; /** Accessible name for each thumb, in order, for example `["Minimum", "Maximum"]`. A single-thumb slider can use `aria-label` or `aria-labelledby`, which are passed to the thumb. */ thumbLabels?: string[]; /** Show a value bubble above the thumb while it is hovered, focused or dragged. */ showValue?: boolean; /** Format the number shown in the bubble and announced as `aria-valuetext`. */ formatValue?: (value: number) => string; }; function Slider({ className, value, defaultValue, min = 0, max = 100, thumbLabels, showValue = false, formatValue, onValueChange, orientation = "horizontal", "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }: SliderProps) { const [inner, setInner] = React.useState<number[]>(defaultValue ?? [min]); const values = value ?? inner; const vertical = orientation === "vertical"; const dir = useDirection(props.dir); return ( <Primitive.Root data-slot="slider" dir={dir} value={value} defaultValue={defaultValue} min={min} max={max} orientation={orientation} onValueChange={(next) => { setInner(next); onValueChange?.(next); }} className={cn( "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-44 data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", className, )} {...props} > <Primitive.Track data-slot="slider-track" className="relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5" > <Primitive.Range data-slot="slider-range" className="absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full" /> </Primitive.Track> {values.map((v, i) => ( <Primitive.Thumb key={i} data-slot="slider-thumb" aria-label={thumbLabels?.[i] ?? (values.length === 1 ? ariaLabel : undefined)} aria-labelledby={ !thumbLabels?.[i] && values.length === 1 ? ariaLabelledby : undefined } aria-valuetext={formatValue ? formatValue(v) : undefined} className="group/thumb relative block size-5 shrink-0 rounded-full border-2 border-primary bg-background shadow-sm outline-none transition-[box-shadow,transform] duration-150 hover:ring-4 hover:ring-ring/30 focus-visible:ring-4 focus-visible:ring-ring/50 active:scale-110 disabled:pointer-events-none motion-reduce:transition-none motion-reduce:active:scale-100" > {showValue && ( <span aria-hidden="true" className={cn( "pointer-events-none absolute rounded-md bg-foreground px-1.5 py-0.5 text-xs font-medium tabular-nums text-background opacity-0 shadow transition-opacity duration-150 group-hover/thumb:opacity-100 group-focus-visible/thumb:opacity-100 group-active/thumb:opacity-100 motion-reduce:transition-none", vertical ? "top-1/2 start-full ms-2 -translate-y-1/2" : "bottom-full left-1/2 mb-2 -translate-x-1/2", )} > {formatValue ? formatValue(v) : v} </span> )} </Primitive.Thumb> ))} </Primitive.Root> ); } export { Slider, type SliderProps };Update the import paths to match your project setup.
Usage
import { Slider } from "@/components/ballmac/slider"The full example is in the Code tab above.
Examples
Vertical and disabled
import { Slider } from "@/components/ballmac/slider";
export default function SliderStates() {
return (
<div className="flex w-full max-w-sm items-end gap-8">
<div className="grid flex-1 gap-6">
<Slider aria-label="Disabled" defaultValue={[40]} disabled />
<Slider aria-label="Step by 25" defaultValue={[50]} step={25} showValue />
</div>
<div className="flex gap-6 pt-2">
<Slider aria-label="Bass" orientation="vertical" defaultValue={[70]} />
<Slider aria-label="Treble" orientation="vertical" defaultValue={[30]} />
</div>
</div>
);
}API reference
| Prop | Type | Default |
|---|---|---|
valueControlled value. One number per thumb. | number[] | — |
defaultValueInitial value when uncontrolled. One number per thumb; two numbers make a range. | number[] | — |
thumbLabelsAccessible name for each thumb, in order, for example `["Minimum", "Maximum"]`. A single-thumb slider can use `aria-label` or `aria-labelledby`, which are passed to the thumb. | string[] | — |
showValueShow a value bubble above the thumb while it is hovered, focused or dragged. | boolean | false |
formatValueFormat the number shown in the bubble and announced as `aria-valuetext`. | (value: number) => string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| ArrowLeft / ArrowRight / ArrowUp / ArrowDown | Changes the value by one step |
| PageUp / PageDown | Changes by a larger step |
| Home / End | Jumps to min or max |
| Tab | Moves between thumbs |
Use with AI
Drag or use the keyboard to pick a number or a range. Give every thumb a name with thumbLabels; showValue adds a bubble on hover, focus and drag. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Slider (@ballmac/slider) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Volume, brightness and zoom
- Price and date ranges with two thumbs
- Any bounded numeric choice where precision is not critical
Not for
- Exact numeric entry; use number-input
- A styled range with marks or steps shown; use slider-range
Registry JSON: https://ui.ballmac.com/r/slider.json
Credits
Based on shadcn/ui Slider, adapted by Ballmac. Free to use in personal and commercial projects.
- npm
- radix-ui
- Registry
- @ballmac/directionshadcn/utils
Pairs well with
Label
An accessible form label built on Radix Label that dims itself when the paired peer control is disabled and never selects text on double-click.
Field
Form field layout with label, description and error that wire their ids to the control automatically, plus fieldset, legend, orientation and invalid/disabled state.
Calendar
A date grid for one day, several days or a range, with month and year selects, week numbers, disabled rules and range-end styling, on React DayPicker.
Color Picker
A native color chooser paired with an editable hex field and a live swatch.