Ir al contenido
Evaluar mi proyecto
Componentes · FormulariosDisponible

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.

animated-toggles.tsx86 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/interruptores-animados

Ejemplo 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.

ejemplo.tsx5 líneas
import { SpringSwitch } from "./animated-toggles";

export default function Ejemplo() {
  return <SpringSwitch label="Ejemplo" checked />;
}

Propiedades de SpringSwitch

SpringSwitch acepta 6 propiedades; 1 es obligatoria.

PropiedadTipoDescripción
labelstringObligatoria.
checked?booleanOpcional.
defaultChecked?booleanOpcional.
onCheckedChange?(checked: boolean) => voidOpcional.
disabled?booleanOpcional.
className?stringOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js (quita la directiva use client fuera de Next.js) · Sin Tailwind

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.