All Taste components
Inputs
Slider
Pick one value or a range on a track, with the current values shown as you drag.
single · range · value bubbles · scale labels
Delivery radius8 km
0 km25 km
Price per plate₹250 – ₹1,200
₹0₹2,000
"use client";
import { useId, useState } from "react";
import { cn } from "@/lib/utils";
/*
* The visible track and thumbs are plain elements; a native <input type="range">
* sits on top, invisible, so pointer, touch, arrow keys, Home/End and screen
* readers all work without custom handling.
*/
const native =
"absolute -left-[9px] inset-y-0 m-0 w-[calc(100%+18px)] cursor-pointer appearance-none bg-transparent opacity-0";
const thumb = "absolute top-1/2 size-[18px] -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-primary bg-bg shadow transition-shadow";
type Base = { label: string; min: number; max: number; step?: number; format?: (v: number) => string };
export function Slider({ label, min, max, step = 1, value, onChange, format = String }:
Base & { value: number; onChange: (v: number) => void }) {
const id = useId();
const [focused, setFocused] = useState(false);
const pct = ((value - min) / (max - min)) * 100;
return (
<div className="grid gap-2.5">
<div className="flex items-baseline justify-between gap-3">
<label htmlFor={id} className="text-[13px] font-medium text-fg">{label}</label>
<span className="text-[13px] tabular-nums text-fg-muted">{format(value)}</span>
</div>
<div className="relative mx-[9px] h-5">
<span className="absolute -inset-x-[9px] top-2 h-1 rounded-full bg-surface-2" />
<span className="absolute -left-[9px] top-2 h-1 rounded-full bg-primary" style={{ width: "calc(" + pct + "% + 9px)" }} />
<span className={cn(thumb, focused && "ring-4 ring-primary/25")} style={{ left: pct + "%" }} />
<input
id={id} type="range" min={min} max={max} step={step} value={value}
aria-valuetext={format(value)}
onChange={(e) => onChange(Number(e.target.value))}
onFocus={() => setFocused(true)} onBlur={() => setFocused(false)}
className={native}
/>
</div>
<div className="flex justify-between text-xs tabular-nums text-fg-subtle"><span>{format(min)}</span><span>{format(max)}</span></div>
</div>
);
}
export function RangeSlider({ label, min, max, step = 1, value, onChange, format = String }:
Base & { value: [number, number]; onChange: (v: [number, number]) => void }) {
const id = useId();
const [focused, setFocused] = useState<0 | 1 | null>(null);
const [lo, hi] = value;
const p = (v: number) => ((v - min) / (max - min)) * 100;
// Both inputs span the track; only their thumbs take the pointer, so either can be dragged.
const dual = cn(native, "pointer-events-none [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:size-5 [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none");
return (
<div role="group" aria-labelledby={id} className="grid gap-1">
<div className="flex items-baseline justify-between gap-3">
<span id={id} className="text-[13px] font-medium text-fg">{label}</span>
<span className="text-[13px] tabular-nums text-fg-muted">{format(lo)} – {format(hi)}</span>
</div>
<div className="relative mx-[9px] mt-8 h-5">
<span className="absolute -inset-x-[9px] top-2 h-1 rounded-full bg-surface-2" />
<span className="absolute top-2 h-1 rounded-full bg-primary" style={{ left: p(lo) + "%", width: p(hi) - p(lo) + "%" }} />
{[lo, hi].map((v, i) => (
<span key={i} className={cn(thumb, focused === i && "ring-4 ring-primary/25")} style={{ left: p(v) + "%" }}>
<span className="absolute bottom-full left-1/2 mb-2 -translate-x-1/2 whitespace-nowrap rounded-sm bg-fg px-1.5 py-0.5 text-xs font-medium tabular-nums text-bg">
{format(v)}
</span>
</span>
))}
<input
type="range" min={min} max={max} step={step} value={lo}
aria-label={"Minimum " + label.toLowerCase()} aria-valuetext={format(lo)}
onChange={(e) => onChange([Math.min(Number(e.target.value), hi - step), hi])}
onFocus={() => setFocused(0)} onBlur={() => setFocused(null)}
className={dual}
/>
<input
type="range" min={min} max={max} step={step} value={hi}
aria-label={"Maximum " + label.toLowerCase()} aria-valuetext={format(hi)}
onChange={(e) => onChange([lo, Math.max(Number(e.target.value), lo + step)])}
onFocus={() => setFocused(1)} onBlur={() => setFocused(null)}
className={dual}
/>
</div>
<div className="mt-1.5 flex justify-between text-xs tabular-nums text-fg-subtle"><span>{format(min)}</span><span>{format(max)}</span></div>
</div>
);
}
// <Slider label="Delivery radius" min={0} max={25} value={km} onChange={setKm} format={(v) => v + " km"} />
// <RangeSlider label="Price per plate" min={0} max={2000} step={50} value={price} onChange={setPrice}
// format={(v) => "₹" + v.toLocaleString("en-IN")} />
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.