Checkbox
BetaMarca una o varias opciones independientes. Tres tamaños, estado indeterminado y texto de apoyo.
registry/ds/checkbox.tsximport { Checkbox } from "@/registry/ds/checkbox";
export default function CheckboxBasic() {
return <Checkbox defaultChecked label="Acepto los términos y condiciones" />;
}Cuándo usarlo
Cada checkbox es una decisión independiente: marcar uno no afecta a los demás. Se confirma después, con un botón.
| Usá Checkbox para… | Usá otra cosa para… |
|---|---|
| Elegir varias opciones de una lista. | Elegir una sola de varias → Radio. |
| Aceptar términos, consentir algo. | Prender algo con efecto inmediato → Switch. |
| Seleccionar filas de una tabla. | Más de ~7 opciones → Multi-select. |
Anatomía
- 1Control — La caja. Es lo único que cambia de estado visual.
- 2Label — Qué se está aceptando o eligiendo. También es clickeable.
- 3Texto de apoyo — Opcional. Aclara consecuencias o condiciones.
Estados
El modelo es el mismo que en Figma: Checked (qué dice la caja) × State (cómo se está interactuando).
import { Checkbox } from "@/registry/ds/checkbox";
export default function CheckboxStates() {
return (
<div className="grid grid-cols-2 gap-x-10 gap-y-4 sm:grid-cols-3">
<Checkbox label="Sin marcar" />
<Checkbox defaultChecked label="Marcado" />
<Checkbox indeterminate label="Indeterminado" />
<Checkbox disabled label="Deshabilitado" />
<Checkbox disabled defaultChecked label="Marcado, deshabilitado" />
<Checkbox disabled indeterminate label="Indeterminado, deshabilitado" />
</div>
);
}Indeterminado no es un tercer valor que la persona elige: es un resumen. Aparece en un checkbox padre cuando algunos de sus hijos están marcados.
"use client";
import { useState } from "react";
import { Checkbox } from "@/registry/ds/checkbox";
const materias = ["Cálculo I", "Física I", "Programación"];
export default function CheckboxGroup() {
const [selected, setSelected] = useState<string[]>(["Cálculo I"]);
const all = selected.length === materias.length;
return (
<div className="flex flex-col gap-3">
{/* El padre resume a los hijos: indeterminado cuando hay algunos, no todos */}
<Checkbox
label="Todas las materias"
checked={all}
indeterminate={selected.length > 0 && !all}
onCheckedChange={(checked) => setSelected(checked ? materias : [])}
/>
<div className="ml-7 flex flex-col gap-3">
{materias.map((m) => (
<Checkbox
key={m}
label={m}
checked={selected.includes(m)}
onCheckedChange={(checked) =>
setSelected((prev) => (checked ? [...prev, m] : prev.filter((x) => x !== m)))
}
/>
))}
</div>
</div>
);
}Con texto de apoyo
import { Checkbox } from "@/registry/ds/checkbox";
export default function CheckboxSupporting() {
return (
<div className="flex flex-col gap-4">
<Checkbox defaultChecked label="Novedades académicas" supporting="Fechas de inscripción, exámenes y cambios de horario." />
<Checkbox label="Vida estudiantil" supporting="Eventos, deportes y actividades del campus." />
<Checkbox disabled label="Avisos de facturación" supporting="Obligatorios para estudiantes activos." />
</div>
);
}Tamaños
import { Checkbox } from "@/registry/ds/checkbox";
export default function CheckboxSizes() {
return (
<div className="flex flex-wrap items-start gap-8">
<Checkbox size="sm" defaultChecked label="Small" />
<Checkbox size="md" defaultChecked label="Medium" />
<Checkbox size="lg" defaultChecked label="Large" />
</div>
);
}md es el default. sm para tablas y UI densa; lg para mobile y
formularios con pocas opciones. Dentro de un mismo grupo, un solo tamaño.
Contenido
- El label describe lo que pasa al marcarlo, en positivo.
- Sin punto final. Mayúscula solo al inicio.
- Si necesitás explicar una consecuencia, va en el texto de apoyo, no en el label.
- Listas verticales: se escanean mejor que en fila.
En positivo: marcado significa "sí, quiero esto".
Doble negación: nadie sabe qué pasa al marcarlo.
Medidas
| Size | Caja | Glifo | Label | Apoyo | Gap |
|---|---|---|---|---|---|
sm | 16 | 12 | Text sm / Medium | Text sm / Regular | spacing-md 8 |
md | 20 | 14 | Text md / Medium | Text sm / Regular | spacing-md 8 |
lg | 24 | 16 | Text md / Medium | Text sm / Regular | spacing-md 8 |
Radio de la caja: radius-sm (6). Borde: border-sm (1). El área clickeable
es toda la fila (control + textos), no solo la caja.
Tokens por estado
| Parte | Propiedad | Default | Hover | Pressed |
|---|---|---|---|---|
| Caja sin marcar | Fondo | bg/elevated | bg/elevated | bg/elevated |
| Caja sin marcar | Borde | border/default | border/strong | border/strong |
| Caja marcada | Fondo | primary/solid | primary/solid-hover | primary/solid-active |
| Check / guion | Color | primary/on-solid | primary/on-solid | primary/on-solid |
| Parte | Propiedad | Token |
|---|---|---|
| Foco | Anillo 2 px | focus/ring |
| Deshabilitado | Fondo | bg/subtle |
| Deshabilitado | Borde | border/subtle |
| Deshabilitado | Check y textos | text/disabled |
| Label | Color | text/default |
| Texto de apoyo | Color | text/muted |
En Figma
Dos componentes: Checkbox (solo la caja, para tablas o composiciones) y
Checkbox item (caja + textos). En código es uno solo: si pasás label, es
el item.
| Propiedad en Figma | Valores | En código | Nota |
|---|---|---|---|
| Size | sm · md · lg | size | — |
| Checked | Unchecked · Checked · Indeterminate | checked / indeterminate | — |
| State | Default · Hover · Focus · Pressed · Disabled | disabled | El resto de los estados son automáticos. |
| Label | Texto | label | — |
| Supporting · Show supporting | Texto · on/off | supporting | En código, pasar el texto lo muestra. |
Instalación
npx shadcn@latest add https://polen.cdc.cool/r/checkbox.jsonPlayground
Configurá el componente y copiá el snippet: se genera del mismo estado que renderiza el preview.
<Checkbox label="Acepto los términos y condiciones" defaultChecked />Ejemplos
Controlado, con padre indeterminado
"use client";
import { useState } from "react";
import { Checkbox } from "@/registry/ds/checkbox";
const materias = ["Cálculo I", "Física I", "Programación"];
export default function CheckboxGroup() {
const [selected, setSelected] = useState<string[]>(["Cálculo I"]);
const all = selected.length === materias.length;
return (
<div className="flex flex-col gap-3">
{/* El padre resume a los hijos: indeterminado cuando hay algunos, no todos */}
<Checkbox
label="Todas las materias"
checked={all}
indeterminate={selected.length > 0 && !all}
onCheckedChange={(checked) => setSelected(checked ? materias : [])}
/>
<div className="ml-7 flex flex-col gap-3">
{materias.map((m) => (
<Checkbox
key={m}
label={m}
checked={selected.includes(m)}
onCheckedChange={(checked) =>
setSelected((prev) => (checked ? [...prev, m] : prev.filter((x) => x !== m)))
}
/>
))}
</div>
</div>
);
}Con texto de apoyo
import { Checkbox } from "@/registry/ds/checkbox";
export default function CheckboxSupporting() {
return (
<div className="flex flex-col gap-4">
<Checkbox defaultChecked label="Novedades académicas" supporting="Fechas de inscripción, exámenes y cambios de horario." />
<Checkbox label="Vida estudiantil" supporting="Eventos, deportes y actividades del campus." />
<Checkbox disabled label="Avisos de facturación" supporting="Obligatorios para estudiantes activos." />
</div>
);
}Props
Construido sobre Base UI Checkbox:
acepta todas sus props (name, value, required, inputRef…) y participa
de formularios nativos con un <input> oculto.
| Prop | Tipo | Default | Descripción |
|---|---|---|---|
size | "sm" | "md" | "lg" | "md" | Caja de 16 / 20 / 24 px. |
label | ReactNode | — | Texto del item. Sin label el control necesita aria-label. |
supporting | ReactNode | — | Línea de apoyo bajo el label. |
checked | boolean | — | Valor controlado. |
defaultChecked | boolean | false | Valor inicial, no controlado. |
indeterminate | boolean | false | Estado mixto: muestra un guion y se anuncia como 'mixed'. |
onCheckedChange | (checked: boolean) => void | — | Se llama al cambiar. |
disabled | boolean | false | Bloquea la interacción y atenúa control y textos. |
Teclado
| Tecla | Qué hace |
|---|---|
| Tab | Mueve el foco al checkbox. |
| Espacio | Marca o desmarca. |
Qué viene resuelto
role="checkbox"conaria-checkedentrue,falseomixed.- Label asociado: hacer clic en el texto marca la caja.
- Un
<input type="checkbox">oculto lo hace funcionar en formularios nativos. - Focus ring visible solo con teclado.
- El área interactiva es la fila completa, así que supera los 24 px mínimos
aun con la caja
smde 16.
Qué tenés que cuidar vos
- Sin
label, pasáaria-label(por ejemplo, en la celda de una tabla). - Agrupá checkboxes relacionados en un
<fieldset>con su<legend>. - El error de validación va en texto, cerca del grupo — no solo en color.