Slider
Un slider que siempre muestra su valor y sus extremos, con soporte completo de teclado.
Delivery radius
Discount
Instalación
$ pnpm dlx shadcn@latest add @ssntl/sliderUso
import { Slider } from "@/components/ui/slider"
<Slider label="Radio de entrega" min={1} max={30} defaultValue={8} formatValue={(km) => `${km} km`} />- Valor claro: el valor actual, con su unidad, va junto a la etiqueta; los dos extremos se rotulan bajo la barra.
- formatValue también define aria-valuetext, así los lectores de pantalla dicen “8 km” y no “8”.
- Las flechas, Re Pág/Av Pág, Inicio y Fin mueven el control (Radix). Usa un slider para valores aproximados; los exactos se escriben.
Código
"use client"
import * as React from "react"
import { Slider as SliderPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
// Value clarity: a slider always says where it is. The label and the current
// value share a row above the track, and the ends are labelled below, so
// nobody has to drag to find out what a position means. Radix handles the
// keyboard (arrows, Page Up/Down, Home, End) and aria-valuenow.
function Slider({
label,
formatValue = String,
min = 0,
max = 100,
value,
defaultValue,
onValueChange,
className,
...props
}: Omit<
React.ComponentProps<typeof SliderPrimitive.Root>,
"value" | "defaultValue" | "onValueChange"
> & {
/** The visible label; it also names the thumb. */
label: string
/** How the value reads, with its unit: (v) => `${v} years`. */
formatValue?: (value: number) => string
value?: number
defaultValue?: number
onValueChange?: (value: number) => void
}) {
const id = React.useId()
const [inner, setInner] = React.useState(defaultValue ?? min)
const current = value ?? inner
return (
<div data-slot="slider" className={cn("grid gap-3", className)}>
<div className="flex items-baseline justify-between gap-4">
<span id={id} className="text-label">
{label}
</span>
<output aria-hidden className="text-label font-medium tabular-nums">
{formatValue(current)}
</output>
</div>
<SliderPrimitive.Root
min={min}
max={max}
value={[current]}
onValueChange={([next = min]) => {
setInner(next)
onValueChange?.(next)
}}
className="relative flex h-6 touch-none items-center select-none data-disabled:opacity-40"
{...props}
>
<SliderPrimitive.Track className="relative h-1.5 grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb
aria-labelledby={id}
aria-valuetext={formatValue(current)}
className="block size-6 rounded-full border-2 border-primary bg-card shadow-float transition-[scale] duration-(--duration-fast) ease-out hover:scale-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring active:scale-(--es-press) motion-reduce:transition-none"
/>
</SliderPrimitive.Root>
<div
aria-hidden
className="flex justify-between text-caption text-muted-foreground"
>
<span>{formatValue(min)}</span>
<span>{formatValue(max)}</span>
</div>
</div>
)
}
export { Slider }