Componentform
Dual Slider
Price range selector with minimum and maximum draggable handles.
Preview
Live Preview
PRICE RANGE
$35—$120
Installation
Tokens guide →Add to your project via CLI (zero dependencies, code you own):
npx bigbullui add dual-sliderOr install the full npm package:
npm install bigbulluiUsage
import { DualSlider } from "@/components/ui/dual-slider";
<DualSlider min={10} max={200} defaultValue={[30, 100]} />Source code
dual-slider.tsxZero dependencies (only React + utils). Copy and paste directly into your project:
"use client";
import * as React from "react";
import { cn } from "./lib/utils";
export interface DualSliderProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange" | "defaultValue"> {
min?: number;
max?: number;
step?: number;
value?: [number, number];
defaultValue?: [number, number];
onValueChange?: (val: [number, number]) => void;
currency?: string;
className?: string;
}
export function DualSlider({
min = 0,
max = 100,
step = 1,
value: controlledValue,
defaultValue = [20, 80],
onValueChange,
currency = "$",
className,
...props
}: DualSliderProps) {
const [internalVal, setInternalVal] = React.useState<[number, number]>(defaultValue);
const isControlled = controlledValue !== undefined;
const [valMin, valMax] = isControlled ? controlledValue : internalVal;
const handleMinChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newMin = Math.min(Number(e.target.value), valMax - step);
const updated: [number, number] = [newMin, valMax];
if (!isControlled) setInternalVal(updated);
onValueChange?.(updated);
};
const handleMaxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newMax = Math.max(Number(e.target.value), valMin + step);
const updated: [number, number] = [valMin, newMax];
if (!isControlled) setInternalVal(updated);
onValueChange?.(updated);
};
const minPercent = ((valMin - min) / (max - min)) * 100;
const maxPercent = ((valMax - min) / (max - min)) * 100;
return (
<div className={cn("w-full space-y-3 font-mono select-none", className)} {...props}>
{/* Price Header */}
<div className="flex items-center justify-between text-xs font-bold text-foreground">
<span className="text-[10px] text-muted-foreground uppercase tracking-widest">
PRICE RANGE
</span>
<div className="flex items-center gap-1.5 rounded-sm border border-dashed border-border bg-secondary px-2 py-0.5 text-accent">
<span>{currency}{valMin}</span>
<span>—</span>
<span>{currency}{valMax}</span>
</div>
</div>
{/* Dual Slider Track */}
<div className="relative flex h-6 w-full items-center">
{/* Background Track */}
<div className="absolute h-2 w-full rounded-xs bg-secondary border border-border" />
{/* Selected Range Highlight */}
<div
style={{
left: `${minPercent}%`,
width: `${maxPercent - minPercent}%`,
}}
className="absolute h-2 bg-accent shadow-xs"
/>
{/* Range Inputs Overlaid */}
<input
type="range"
min={min}
max={max}
step={step}
value={valMin}
onChange={handleMinChange}
aria-label="Minimum price"
className="pointer-events-none absolute h-2 w-full appearance-none bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:size-4.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-xs [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-foreground [&::-webkit-slider-thumb]:bg-card [&::-webkit-slider-thumb]:shadow-xs [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:hover:scale-110"
/>
<input
type="range"
min={min}
max={max}
step={step}
value={valMax}
onChange={handleMaxChange}
aria-label="Maximum price"
className="pointer-events-none absolute h-2 w-full appearance-none bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:size-4.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-xs [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-foreground [&::-webkit-slider-thumb]:bg-card [&::-webkit-slider-thumb]:shadow-xs [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:hover:scale-110"
/>
</div>
</div>
);
}
Props
| Prop | Type | Description |
|---|---|---|
| min | number | Minimum limit (default 0). |
| max | number | Maximum limit (default 100). |
| value | [number, number] | Controlled range tuple. |
| defaultValue | [number, number] | Initial range tuple (default [20, 80]). |
| onValueChange | (val: [number, number]) => void | Callback when range changes. |
| currency | string | Currency symbol (default '$'). |