Casillas con check dibujado
Casillas cuyo trazo se dibuja al marcar y tachan el texto; variantes de trazo, relleno y círculo sobre un input nativo.
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";
/** Casillas animadas: el trazo del check se dibuja y la caja cambia de forma. Tres estilos (trazo, relleno y círculo). Casilla nativa debajo, así que mantiene teclado y formularios. React + TypeScript, sin dependencias. */
export type CasillaDibujadaItem = { id: string; label: string; checked?: boolean };
export type CasillaDibujadaProps = {
items?: CasillaDibujadaItem[];
variant?: "trazo" | "relleno" | "circulo";
onChange?: (id: string, checked: boolean) => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gccsd-root { box-sizing: border-box; display: grid; gap: 14px; width: 100%; max-width: 360px; color: var(--gccsd-fg, #1a1a1a); font: 500 16px/1.3 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gccsd-root *, .gccsd-root *::before, .gccsd-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gccsd-root *, .gccsd-root *::before, .gccsd-root *::after { animation: none !important; transition-duration: .01ms !important; } }
.gccsd-item { position: relative; display: flex; align-items: center; gap: 14px; cursor: pointer; min-height: 32px; }
.gccsd-input { position: absolute; width: 28px; height: 28px; margin: 0; opacity: 0; cursor: pointer; }
.gccsd-box { flex-shrink: 0; width: 28px; height: 28px; transition: transform .2s cubic-bezier(.16, 1, .3, 1); }
.gccsd-box rect, .gccsd-box circle { fill: transparent; stroke: var(--gccsd-fg, #1a1a1a); stroke-width: 2; transition: fill .25s, stroke .25s; }
.gccsd-box path { fill: none; stroke: var(--gccsd-on, #fff); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 26; stroke-dashoffset: 26; transition: stroke-dashoffset .3s cubic-bezier(.65, 0, .35, 1) 0s; }
.gccsd-input:checked + .gccsd-box path { stroke-dashoffset: 0; transition-delay: .12s; }
.gccsd-input:checked + .gccsd-box rect, .gccsd-input:checked + .gccsd-box circle { fill: var(--gccsd-fg, #1a1a1a); }
.gccsd-root[data-v="trazo"] .gccsd-box path { stroke: var(--gccsd-fg, #1a1a1a); }
.gccsd-root[data-v="trazo"] .gccsd-input:checked + .gccsd-box rect { fill: transparent; stroke-dasharray: 80; animation: gccsd-pop .35s cubic-bezier(.34, 1.56, .64, 1); }
.gccsd-root[data-v="circulo"] .gccsd-input:checked + .gccsd-box circle { fill: var(--gccsd-accent, #16a34a); stroke: var(--gccsd-accent, #16a34a); }
.gccsd-input:active + .gccsd-box { transform: scale(.88); }
.gccsd-input:focus-visible + .gccsd-box { outline: 3px solid #2563eb; outline-offset: 3px; border-radius: 6px; }
.gccsd-text { position: relative; transition: color .25s, opacity .25s; }
.gccsd-text::after { content: ""; position: absolute; left: 0; right: 0; top: 52%; height: 2px; background: currentColor; transform: scaleX(0); transform-origin: left; transition: transform .35s cubic-bezier(.65, 0, .35, 1); }
.gccsd-input:checked ~ .gccsd-text { opacity: .5; }
.gccsd-input:checked ~ .gccsd-text::after { transform: scaleX(1); }
@keyframes gccsd-pop { 40% { transform: scale(1.12); } }`;
const DEFAULT_ITEMS: CasillaDibujadaItem[] = [
{ id: "a", label: "Revisar la propuesta", checked: true },
{ id: "b", label: "Enviar los accesos" },
{ id: "c", label: "Agendar la llamada de arranque" },
];
export function CasillaDibujada({ items = DEFAULT_ITEMS, variant = "trazo", onChange, className = "", style }: CasillaDibujadaProps) {
const uid = useId();
const [state, setState] = useState<Record<string, boolean>>(() => Object.fromEntries(items.map((item) => [item.id, !!item.checked])));
return (
<div className={`gccsd-root ${className}`} style={style} data-v={variant}>
<style>{CSS}</style>
{items.map((item) => (
<label key={item.id} className="gccsd-item" htmlFor={`${uid}-${item.id}`}>
<input
id={`${uid}-${item.id}`}
className="gccsd-input"
type="checkbox"
checked={!!state[item.id]}
onChange={(event) => {
setState((prev) => ({ ...prev, [item.id]: event.target.checked }));
onChange?.(item.id, event.target.checked);
}}
/>
<svg className="gccsd-box" viewBox="0 0 28 28" aria-hidden="true">
{variant === "circulo" ? <circle cx="14" cy="14" r="12" /> : <rect x="2" y="2" width="24" height="24" rx={variant === "relleno" ? 7 : 3} />}
<path d="m8 14.5 4 4 8-9" />
</svg>
<span className="gccsd-text">{item.label}</span>
</label>
))}
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/casilla-dibujadaEjemplo de uso de Casillas con check dibujado
Después de copiar casilla-dibujada.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { CasillaDibujada } from "./casilla-dibujada";
export default function Ejemplo() {
return <CasillaDibujada variant="trazo" />;
}Propiedades de CasillaDibujada
CasillaDibujada acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
items? | CasillaDibujadaItem[] | Opcional. |
variant? | "trazo" | "relleno" | "circulo" | Opcional. |
onChange? | (id: string, checked: boolean) => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Variantes disponibles
variant:trazo,relleno,circulo
Personaliza Casillas con check dibujado 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 |
|---|---|
--gccsd-fg | #1a1a1a |
--gccsd-on | #fff |
--gccsd-accent | #16a34a |
Accesibilidad y rendimiento
El archivo tiene 72 líneas (4 KB) y define 1 animación con @keyframes. Esto es lo que hace Casillas con check dibujado 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).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Es un componente de cliente de Next.js: incluye la directiva "use client".