A two-thumb Radix slider with a labeled value span, keyboard operation, and minimum thumb spacing.
Filter by monthly spend
Use arrow keys for precise adjustments.
"use client"
import { SliderRange } from "@/components/ballmac/slider-range"
export default function SliderRangeDemo() {
return (
<div className="w-full max-w-sm rounded-xl border border-border bg-card p-5">
<p className="mb-5 text-sm font-semibold">Filter by monthly spend</p>
<SliderRange
label="Budget range"
min={0}
max={500}
step={10}
defaultValue={[80, 320]}
minStepsBetweenThumbs={2}
formatValue={(value) => `$${value}`}
/>
<p className="text-muted-foreground mt-4 text-xs">
Use arrow keys for precise adjustments.
</p>
</div>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/slider-rangeInstall the dependencies.
$ pnpm add radix-uiAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/direction @ballmac/i18nCopy the source into your project.
components/ballmac/slider-range.tsx// Ballmac UI: Slider Range. https://ui.ballmac.com/components/slider-range "use client" import * as React from "react" import { Slider as SliderPrimitive } from "radix-ui" import { useDirection } from "@/lib/ballmac/direction" import { cn } from "@/lib/utils" import { useMessages } from "@/lib/ballmac/i18n" type SliderRangeProps = Omit< React.ComponentProps<typeof SliderPrimitive.Root>, "value" | "defaultValue" | "onValueChange" > & { /** Visible label for the range. */ label: string /** Controlled lower and upper values. */ value?: [number, number] /** Initial lower and upper values. */ defaultValue?: [number, number] /** Called when either thumb changes. */ onValueChange?: (value: [number, number]) => void /** Formats both displayed values. */ formatValue?: (value: number) => string /** Minimum number of steps between thumbs. */ minStepsBetweenThumbs?: number } function SliderRange({ className, label, value, defaultValue = [25, 75], onValueChange, formatValue = (number) => String(number), min = 0, max = 100, step = 1, minStepsBetweenThumbs = 1, disabled, ...props }: SliderRangeProps) { const msg = useMessages() const [internal, setInternal] = React.useState<[number, number]>(defaultValue) const current = value ?? internal const dir = useDirection(props.dir) return ( <div data-slot="slider-range" className={cn("flex min-w-0 flex-col gap-4", className)} > <div className="flex items-center justify-between gap-3 text-sm"> <span className="font-medium">{label}</span> <span data-slot="slider-range-value" className="text-muted-foreground shrink-0 font-mono text-xs tabular-nums" > {formatValue(current[0])} – {formatValue(current[1])} </span> </div> <SliderPrimitive.Root dir={dir} {...props} value={current} onValueChange={(next) => { const pair: [number, number] = [next[0], next[1]] if (value === undefined) setInternal(pair) onValueChange?.(pair) }} min={min} max={max} step={step} minStepsBetweenThumbs={minStepsBetweenThumbs} disabled={disabled} className="relative flex h-5 w-full touch-none select-none items-center data-[disabled]:opacity-50" > <SliderPrimitive.Track data-slot="slider-range-track" className="bg-secondary relative h-2 w-full grow overflow-hidden rounded-full" > <SliderPrimitive.Range data-slot="slider-range-fill" className="bg-primary absolute h-full" /> </SliderPrimitive.Track> <SliderPrimitive.Thumb data-slot="slider-range-min" aria-label={msg("slider-range.minimum", "{label} minimum", { label })} className="border-primary bg-background block size-5 rounded-full border-2 shadow-sm outline-none transition-transform duration-150 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-safe:hover:scale-110 motion-reduce:transition-none" /> <SliderPrimitive.Thumb data-slot="slider-range-max" aria-label={msg("slider-range.maximum", "{label} maximum", { label })} className="border-primary bg-background block size-5 rounded-full border-2 shadow-sm outline-none transition-transform duration-150 focus-visible:ring-[3px] focus-visible:ring-ring/50 motion-safe:hover:scale-110 motion-reduce:transition-none" /> </SliderPrimitive.Root> <div className="text-muted-foreground flex justify-between text-xs tabular-nums"> <span>{formatValue(min)}</span> <span>{formatValue(max)}</span> </div> </div> ) } export { SliderRange, type SliderRangeProps }Update the import paths to match your project setup.
Usage
import { SliderRange } from "@/components/ballmac/slider-range"The full example is in the Code tab above.
Examples
States and variants
"use client"
import { SliderRange } from "@/components/ballmac/slider-range"
export default function SliderRangeStates() {
return (
<div className="grid w-full max-w-xs gap-6">
<SliderRange
label="Duration in days"
min={1}
max={30}
defaultValue={[7, 21]}
formatValue={(value) => `${value}d`}
/>
<SliderRange label="Disabled range" defaultValue={[20, 60]} disabled />
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
label*Visible label for the range. | string | — |
valueControlled lower and upper values. | [number, number] | — |
defaultValueInitial lower and upper values. | [number, number] | [25, 75] |
onValueChangeCalled when either thumb changes. | (value: [number, number]) => void | — |
formatValueFormats both displayed values. | (value: number) => string | (number) => String(number) |
minStepsBetweenThumbsMinimum number of steps between thumbs. | number | 1 |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Arrow keys / Home / End / Page Up / Page Down | Adjusts the focused minimum or maximum thumb |
Use with AI
A two-thumb Radix slider with a labeled value span, keyboard operation, and minimum thumb spacing. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Slider Range (@ballmac/slider-range) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Filter a bounded numeric interval
Not for
- Use Number Input for exact values
Registry JSON: https://ui.ballmac.com/r/slider-range.json
Credits
Free to use in personal and commercial projects.
- npm
- radix-ui
Pairs well with
Number Input
A bounded numeric field with native spinbutton semantics, step buttons, and controlled or uncontrolled 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.
Combobox
A searchable single-choice select with groups, descriptions, keywords, clearable value, invalid state and hidden-input form submission.