Interruptor y casilla animados
Interruptor con rebote elástico y casilla cuya marca se dibuja.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
/**
* Interruptor con rebote y casilla que se dibuja.
*
* <SpringSwitch label="Notificaciones" defaultChecked onCheckedChange={(on) => console.log(on)} />
* <DrawCheckbox label="Acepto los términos" name="terms" />
*
* SpringSwitch es un botón con role="switch". DrawCheckbox usa un <input type="checkbox"> real, así que
* funciona dentro de un <form> sin código extra.
*/
import { useId, useState, type InputHTMLAttributes } from "react";
const CSS = `
.gct-row { --gct-on: #16a34a; --gct-off: #c9ccd3; --gct-ink: #17181c; display: inline-flex; align-items: center; gap: 14px; color: var(--gct-ink); font-family: inherit; font-size: 16px; line-height: 1.3; cursor: pointer; user-select: none; }
.gct-switch { position: relative; flex: none; width: 56px; height: 32px; padding: 0; border: 0; border-radius: 999px; background: var(--gct-off); cursor: pointer; transition: background-color .3s; }
.gct-switch[aria-checked="true"] { background: var(--gct-on); }
.gct-thumb { position: absolute; top: 3px; left: 3px; width: 26px; height: 26px; border-radius: 999px; background: #fff; box-shadow: 0 2px 5px rgba(0, 0, 0, .28);
transition: transform .45s cubic-bezier(.34, 1.7, .5, 1), width .2s ease-out; }
.gct-switch:active .gct-thumb { width: 34px; }
.gct-switch[aria-checked="true"] .gct-thumb { transform: translateX(24px); }
.gct-switch[aria-checked="true"]:active .gct-thumb { transform: translateX(16px); }
.gct-switch:focus-visible { outline: 2px solid var(--gct-on); outline-offset: 3px; }
.gct-switch:disabled { opacity: .5; cursor: not-allowed; }
.gct-check { position: relative; display: inline-grid; flex: none; width: 26px; height: 26px; }
.gct-check input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.gct-box { width: 26px; height: 26px; box-sizing: border-box; border: 2px solid #9aa0ab; border-radius: 8px; background: #fff; transition: background-color .25s, border-color .25s, transform .3s cubic-bezier(.34, 1.7, .5, 1); }
.gct-tick { position: absolute; inset: 0; width: 26px; height: 26px; fill: none; stroke: #fff; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 22; stroke-dashoffset: 22; transition: stroke-dashoffset .35s cubic-bezier(.16, 1, .3, 1) .05s; pointer-events: none; }
.gct-check input:checked ~ .gct-box { background: var(--gct-on); border-color: var(--gct-on); transform: scale(1.06); }
.gct-check input:checked ~ .gct-tick { stroke-dashoffset: 0; }
.gct-check input:focus-visible ~ .gct-box { outline: 2px solid var(--gct-on); outline-offset: 3px; }
.gct-check input:disabled ~ .gct-box { opacity: .5; }
@media (prefers-reduced-motion: reduce) { .gct-switch, .gct-thumb, .gct-box, .gct-tick { transition: none; } }
`;
export type SpringSwitchProps = {
label: string;
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
disabled?: boolean;
className?: string;
};
export function SpringSwitch({ label, checked, defaultChecked = false, onCheckedChange, disabled, className }: SpringSwitchProps) {
const [inner, setInner] = useState(defaultChecked);
const on = checked ?? inner;
const id = useId();
function flip() {
if (checked === undefined) setInner(!on);
onCheckedChange?.(!on);
}
return (
<>
<style>{CSS}</style>
<span className={["gct-row", className].filter(Boolean).join(" ")}>
<button id={id} type="button" role="switch" aria-checked={on} className="gct-switch" disabled={disabled} onClick={flip}>
<span className="gct-thumb" />
</button>
<label htmlFor={id}>{label}</label>
</span>
</>
);
}
export type DrawCheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type"> & { label: string };
export function DrawCheckbox({ label, className, ...props }: DrawCheckboxProps) {
return (
<>
<style>{CSS}</style>
<label className={["gct-row", className].filter(Boolean).join(" ")}>
<span className="gct-check">
<input {...props} type="checkbox" />
<span className="gct-box" />
<svg className="gct-tick" viewBox="0 0 26 26" aria-hidden="true"><path d="M7 13.5l4 4 8-9" /></svg>
</span>
<span>{label}</span>
</label>
</>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/interruptores-animadosEjemplo de uso de Interruptor y casilla animados
Después de copiar animated-toggles.tsx a tu proyecto, impórtalo y colócalo donde lo necesites. Exporta 2 componentes: SpringSwitch, DrawCheckbox.
import { SpringSwitch } from "./animated-toggles";
export default function Ejemplo() {
return <SpringSwitch label="Ejemplo" checked />;
}Propiedades de SpringSwitch
SpringSwitch acepta 6 propiedades; 1 es obligatoria.
| Propiedad | Tipo | Descripción |
|---|---|---|
label | string | Obligatoria. |
checked? | boolean | Opcional. |
defaultChecked? | boolean | Opcional. |
onCheckedChange? | (checked: boolean) => void | Opcional. |
disabled? | boolean | Opcional. |
className? | string | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 86 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Interruptor y casilla animados 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: switch.
- Es un componente de cliente de Next.js: incluye la directiva "use client".
Acerca del producto
Dos controles en un archivo: SpringSwitch, un interruptor cuya perilla se estira y rebota, y DrawCheckbox, una casilla cuya marca se dibuja con un trazo. El interruptor usa role=switch y la casilla es un input real, así que funciona dentro de un formulario.
Incluye
Un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.
- SpringSwitch controlado o no controlado
- DrawCheckbox con input nativo (name, value, required…)
- Color de activación en una variable CSS
- Foco visible y estado deshabilitado
Cómo usarlo
SpringSwitch recibe checked u defaultChecked y onCheckedChange. DrawCheckbox recibe las propiedades normales de un input checkbox.