Deslizador de rango de dos tiradores
Rango mínimo-máximo con dos tiradores nativos superpuestos; los tiradores no se cruzan y se manejan con teclado.
Presupuesto mensual$200 – $700
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useId, useState, type CSSProperties } from "react";
/** Deslizador de rango con dos tiradores, minimalista. Son dos input range nativos superpuestos: teclado, foco y lectores de pantalla funcionan sin código extra. React + TypeScript, sin dependencias. */
export type DeslizadorRangoProps = {
label?: string;
min?: number;
max?: number;
step?: number;
defaultValue?: [number, number];
format?: (value: number) => string;
onChange?: (value: [number, number]) => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcdrg-root { box-sizing: border-box; display: block; width: 100%; max-width: 380px; color: var(--gcdrg-fg, #111); font: 500 14px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcdrg-root *, .gcdrg-root *::before, .gcdrg-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gcdrg-root *, .gcdrg-root *::before, .gcdrg-root *::after { transition-duration: .01ms !important; } }
.gcdrg-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 14px; }
.gcdrg-values { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.gcdrg-wrap { position: relative; height: 28px; }
.gcdrg-rail, .gcdrg-fill { position: absolute; top: 13px; height: 2px; border-radius: 2px; }
.gcdrg-rail { left: 0; right: 0; background: var(--gcdrg-rail, #d6d6d2); }
.gcdrg-fill { background: var(--gcdrg-fg, #111); }
.gcdrg-input { position: absolute; inset: 0; width: 100%; height: 28px; margin: 0; background: none; pointer-events: none; -webkit-appearance: none; appearance: none; outline: 0; }
.gcdrg-input::-webkit-slider-runnable-track { background: none; height: 28px; }
.gcdrg-input::-moz-range-track { background: none; }
.gcdrg-input::-webkit-slider-thumb { -webkit-appearance: none; pointer-events: auto; width: 22px; height: 22px; margin-top: 3px; border-radius: 50%; border: 2px solid var(--gcdrg-fg, #111); background: #fff; cursor: grab; transition: transform .15s cubic-bezier(.16, 1, .3, 1); }
.gcdrg-input::-moz-range-thumb { pointer-events: auto; width: 18px; height: 18px; border-radius: 50%; border: 2px solid var(--gcdrg-fg, #111); background: #fff; cursor: grab; }
.gcdrg-input:hover::-webkit-slider-thumb { transform: scale(1.12); }
.gcdrg-input:active::-webkit-slider-thumb { cursor: grabbing; transform: scale(.96); }
.gcdrg-input:focus-visible::-webkit-slider-thumb { outline: 3px solid #2563eb; outline-offset: 3px; }
.gcdrg-input:focus-visible::-moz-range-thumb { outline: 3px solid #2563eb; outline-offset: 3px; }`;
export function DeslizadorRango({ label = "Presupuesto mensual", min = 0, max = 1000, step = 10, defaultValue = [200, 700], format = (value) => `$${value}`, onChange, className = "", style }: DeslizadorRangoProps) {
const id = useId();
const [[lo, hi], setRange] = useState<[number, number]>(defaultValue);
const pct = (value: number) => ((value - min) / (max - min)) * 100;
function update(next: [number, number]) {
setRange(next);
onChange?.(next);
}
return (
<div className={`gcdrg-root ${className}`} style={style} role="group" aria-labelledby={`${id}-l`}>
<style>{CSS}</style>
<div className="gcdrg-head">
<span id={`${id}-l`}>{label}</span>
<span className="gcdrg-values" aria-live="polite">
{format(lo)} – {format(hi)}
</span>
</div>
<div className="gcdrg-wrap">
<span className="gcdrg-rail" />
<span className="gcdrg-fill" style={{ left: `${pct(lo)}%`, right: `${100 - pct(hi)}%` }} />
<input className="gcdrg-input" type="range" aria-label={`${label}, mínimo`} min={min} max={max} step={step} value={lo} onChange={(event) => update([Math.min(Number(event.target.value), hi - step), hi])} />
<input className="gcdrg-input" type="range" aria-label={`${label}, máximo`} min={min} max={max} step={step} value={hi} onChange={(event) => update([lo, Math.max(Number(event.target.value), lo + step)])} />
</div>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/deslizador-rangoEjemplo de uso de Deslizador de rango de dos tiradores
Después de copiar deslizador-rango.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { DeslizadorRango } from "./deslizador-rango";
export default function Ejemplo() {
return <DeslizadorRango label="Ejemplo" />;
}Propiedades de DeslizadorRango
DeslizadorRango acepta 9 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
label? | string | Opcional. |
min? | number | Opcional. |
max? | number | Opcional. |
step? | number | Opcional. |
defaultValue? | [number, number] | Opcional. |
format? | (value: number) => string | Opcional. |
onChange? | (value: [number, number]) => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Deslizador de rango de dos tiradores con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--gcdrg-fg | #111 |
--gcdrg-rail | #d6d6d2 |
Accesibilidad y rendimiento
El archivo tiene 66 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Deslizador de rango de dos tiradores por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Declara roles ARIA para lectores de pantalla: group.
- Es un componente de cliente de Next.js: incluye la directiva "use client".