Checkbox
Ticks on or off; supports indeterminate when some children are selected. Multi-select lists and filters.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Multi-select with label:
```tsx import { Checkbox } from '@/components/atoms/Checkbox';
<Checkbox label="Email me product updates" checked={optIn} onChange={setOptIn} /> ```
Parent row with partial selection:
```tsx <Checkbox label="Select all" checked={all ? true : some ? 'indeterminate' : false} onChange={toggleAll} /> ```
## Accesibilidad
- Prefer the built-in `label` prop (associates the control). If you omit it, supply an external label via `id`/`htmlFor` or `aria-label`. - `checked="indeterminate"` is for “some children selected” — still expose the result in surrounding UI text when it matters.
### Correcto
- Input nativo `<input type='checkbox'>` maneja el estado — screen readers lo leen correctamente - Touch target de 40x40px (input invisible expandido) cumple WCAG minimum 44px aprox - Label envuelve todo el componente — click en el texto tambien togglea - focus-visible muestra ring solo por teclado, no por click - data-indeterminate + ref.indeterminate para el estado parcial - Disabled remueve del tab order via atributo disabled nativo
### Evitar
- No usar Checkbox sin label en contextos donde no hay otro indicador visual - No simular checkbox con divs — el input nativo ya maneja ARIA internamente - No usar error sin explicar el motivo (combinar con Field o texto de ayuda)
## Cuándo no usar
- Exactly one of many options → `Radio` group. - Instant on/off preference with a switch affordance → `Toggle`.
## Criterio de uso
- Usa checkbox para selecciones independientes o para aceptar una condición explícita; varias opciones pueden quedar activas al mismo tiempo. - El estado indeterminate representa una relación parcial, como “algunos hijos seleccionados”; no lo trates como un tercer valor persistente. - En filtros, aplica la selección sin perder contexto y comunica cuántos resultados cambian cuando el efecto no es obvio.
## Gotchas
- El estado indeterminate es visual: mantén el estado accesible y el texto circundante sincronizados. - No uses checkbox para una decisión mutuamente excluyente; en ese caso el usuario necesita `Radio`. - **Ojo**: Para indeterminate en vanilla, necesitas JS: document.querySelector('.checkbox__input').indeterminate = true. El atributo data-indeterminate activa los estilos CSS pero no el estado nativo.
Uso
import { Checkbox } from '@/components/atoms/Checkbox';
<Checkbox checked={ok} onChange={setOk} label="I agree" />Props
checked: boolean | 'indeterminate'disabled: booleanerror: booleanlabel: stringonChange: (checked: boolean) => void
Gotchas
- a11y
Indeterminate is visual only — keep the accessible checked state in sync for AT.
Anatomía CSS
<div class="checkbox"> <span class="checkbox__box"></span> <span class="checkbox__icon"></span> <span class="checkbox__input"></span> <span class="checkbox__label"></span> </div>
| Clase | Propósito |
|---|---|
checkbox | root |
checkbox--disabled | modifier |
checkbox--error | modifier |
checkbox__box | element |
checkbox__icon | element |
checkbox__input | element |
checkbox__label | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| unchecked | bg | #fafafa | checkbox.bg.unchecked |
| unchecked-hover | bg | #f5f5f5 | checkbox.bg.unchecked.hover |
| checked | bg | #0a0a0a | checkbox.bg.checked |
| checked | fg | #fafafa | checkbox.fg.checked |
| checked-hover | bg | #262626 | checkbox.bg.checked.hover |
| disabled | bg | #f5f5f5 | checkbox.bg.disabled |
| disabled | fg | #525252 | checkbox.fg.disabled |
| disabled | border | #f5f5f5 | checkbox.border.disabled |
| all | border.default | #525252 | checkbox.border.default |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
background-color | var(--duration-150) | var(--easing-out) |
border-color | var(--duration-150) | var(--easing-out) |
box-shadow | var(--duration-150) | var(--easing-out) |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Checkbox
Single size (16px box, 40px touch target).
Native <input type="checkbox"> hidden, visual replacement via label.
States: unchecked, checked, indeterminate, disabled, error
------------------------------------------------------------------------- */
.checkbox {
display: inline-flex;
align-items: center;
gap: 0.5em;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
position: relative;
}
.checkbox--disabled {
cursor: not-allowed;
pointer-events: none;
}
/* Hidden native input with touch target */
.checkbox__input {
position: absolute;
width: 2.5rem;
height: 2.5rem;
margin: 0;
padding: 0;
opacity: 0;
cursor: inherit;
left: -0.75rem;
top: 50%;
transform: translateY(-50%);
}
/* Visual box */
.checkbox__box {
display: flex;
align-items: center;
justify-content: center;
width: 1rem;
height: 1rem;
flex-shrink: 0;
border-radius: 4px;
border: 1.5px solid #525252;
background-color: #fafafa;
transition:
background-color 150ms cubic-bezier(0.22, 1, 0.36, 1),
border-color 150ms cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 150ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Check / minus icon */
.checkbox__icon {
display: flex;
align-items: center;
justify-content: center;
width: 0.75rem;
height: 0.75rem;
color: #fafafa;
}
.checkbox__icon svg {
width: 100%;
height: 100%;
}
/* ---- Hover ---- */
.checkbox__input:hover:not(:disabled) ~ .checkbox__box {
background-color: #f5f5f5;
}
.checkbox__input:checked:hover:not(:disabled) ~ .checkbox__box,
.checkbox__input[data-indeterminate]:hover:not(:disabled) ~ .checkbox__box {
background-color: #262626;
}
/* ---- Checked ---- */
.checkbox__input:checked ~ .checkbox__box,
.checkbox__input[data-indeterminate] ~ .checkbox__box {
background-color: #0a0a0a;
border-color: #0a0a0a;
}
/* ---- Focus ---- */
.checkbox__input:focus-visible ~ .checkbox__box {
box-shadow: 0 0 0 2px #a1a1a1b3;
}
/* ---- Disabled ---- */
.checkbox__input:disabled ~ .checkbox__box {
background-color: #f5f5f5;
border-color: #f5f5f5;
}
.checkbox__input:disabled ~ .checkbox__box .checkbox__icon {
color: #525252;
}
/* ---- Error ---- */
.checkbox--error .checkbox__box {
border-color: #f84131;
}
/* ---- Label ---- */
.checkbox__label {
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 12.8px;
line-height: 1;
color: #0a0a0a;
}
.checkbox--disabled .checkbox__label {
color: #525252;
}
Codigo fuente
import { forwardRef, useRef, useEffect, type InputHTMLAttributes } from 'react'; export type CheckboxProps = { checked?: boolean | 'indeterminate'; disabled?: boolean; error?: boolean; label?: string; className?: string; onChange?: (checked: boolean) => void; } & Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'checked' | 'onChange'>; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } const CheckIcon = () => ( <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"> <path d="M2.5 6L5 8.5L9.5 3.5" /> </svg> ); const MinusIcon = () => ( <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"> <path d="M2.5 6H9.5" /> </svg> ); export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>( ( { checked = false, disabled = false, error = false, label, className, onChange, ...props }, forwardedRef, ) => { const innerRef = useRef<HTMLInputElement>(null); const ref = (forwardedRef as React.RefObject<HTMLInputElement>) || innerRef; const isIndeterminate = checked === 'indeterminate'; const isChecked = checked === true; useEffect(() => { if (ref && 'current' in ref && ref.current) { ref.current.indeterminate = isIndeterminate; } }, [isIndeterminate, ref]); const classes = cn( 'checkbox', disabled && 'checkbox--disabled', error && 'checkbox--error', className, ); return ( <label className={classes}> <input ref={ref} type="checkbox" className="checkbox__input" checked={isChecked || isIndeterminate} disabled={disabled} onChange={(e) => onChange?.(e.target.checked)} {...(isIndeterminate ? { 'data-indeterminate': '' } : {})} {...props} /> <span className="checkbox__box"> {isChecked && ( <span className="checkbox__icon"> <CheckIcon /> </span> )} {isIndeterminate && ( <span className="checkbox__icon"> <MinusIcon /> </span> )} </span> {label && <span className="checkbox__label">{label}</span>} </label> ); }, ); Checkbox.displayName = 'Checkbox';