Checkbox
ProntoMarca 0 ou mais opções de uma lista. Suporta estado indeterminate (parent de filhos parcialmente marcados).
Usar quando
Múltipla seleção em listas de opções. Aceitar termos. Ativar funcionalidades opcionais independentes.
Não usar quando
Apenas 1 de N (use Radio). Liga/desliga isolado (use Toggle). Lista enorme (use Combobox multi).
Variantes
Estados
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Check pop-in | check | Background fade ink, ícone ✓ aparece com scale 0→1 | 200ms cubic-bezier(0.32,0.72,0,1) |
| Indeterminate | indeterminate=true | Mostra ícone Minus (−) no lugar do check | 150ms ease |
| Hover border | mouseenter | Borda border → borderStrong | 150ms ease |
| Focus ring | focus-visible | Outline 2px borderInk com offset 2px | instant |
| Click ripple (sutil) | click | Scale 0.9 no contêiner por 100ms | 100ms |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Move foco para o checkbox |
| Space | Marca/desmarca |
| Shift+Tab | Move foco anterior |
ARIA esperado
- <input type="checkbox" id="x" /> <label htmlFor="x">...</label>
- aria-checked="true" | "false" | "mixed" (mixed = indeterminate)
- aria-disabled="true" se disabled
- aria-describedby="hint" se houver dica
Notas
- Use sempre <label htmlFor> — clique no label deve marcar.
- Indeterminate = "mixed" no aria-checked.
- NÃO use checkbox para liga/desliga isolado (use Toggle).
- Em listas hierárquicas, parent indeterminate quando alguns filhos estão marcados.
Código
'use client';
import { useEffect, useRef } from 'react';
import { Check, Minus } from 'lucide-react';
import { T, TYPE, SP, RADIUS, MOTION, transition } from '@/lib/tokens';
interface CheckboxProps {
checked: boolean;
indeterminate?: boolean;
onChange: (checked: boolean) => void;
label: string;
disabled?: boolean;
}
export function Checkbox({ checked, indeterminate, onChange, label, disabled }: CheckboxProps) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (ref.current) ref.current.indeterminate = !!indeterminate;
}, [indeterminate]);
const showCheck = checked && !indeterminate;
const showIndeterminate = checked && indeterminate;
return (
<label style={{ display: 'inline-flex', alignItems: 'center', gap: SP[2], cursor: disabled ? 'not-allowed' : 'pointer' }}>
<input
ref={ref}
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
disabled={disabled}
style={{ position: 'absolute', opacity: 0 }}
/>
<span style={{
width: 18, height: 18, borderRadius: RADIUS.sm,
background: showCheck || showIndeterminate ? T.ink : T.surface,
border: `1px solid ${showCheck || showIndeterminate ? T.ink : T.border}`,
display: 'grid', placeItems: 'center',
transition: transition('all', 'fast'),
opacity: disabled ? 0.5 : 1,
}}>
{showCheck && <Check size={12} color={T.surface} />}
{showIndeterminate && <Minus size={12} color={T.surface} />}
</span>
<span style={{ fontSize: TYPE.base, color: T.ink2, opacity: disabled ? 0.5 : 1 }}>{label}</span>
</label>
);
}Regras
Faça
- ✓Use <label> envolvendo input + texto.
- ✓Background ink quando checked (estilo Apple).
- ✓Indeterminate em parents com filhos parcialmente marcados.
- ✓Permita clique tanto no input quanto no label.
- ✓Tamanho 18×18 (toque confortável em mobile, 44×44 com label).
Não faça
- ✗Não use border colorida só pra marcar checked.
- ✗Não esconda label.
- ✗Não use checkbox isolado para configuração on/off (use Toggle).
- ✗Não use forma redonda (isso é Radio).
- ✗Não use cor brass — checkbox é neutro (ink).
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.ink | #0A0A0A | background quando checked |
T.surface | #FFFFFF | background unchecked + ícone ✓ |
T.border | #E3DFD2 | borda unchecked |
T.ink2 | #1A1A1C | cor do label |
RADIUS.sm | 6 | borderRadius |
MOTION.fast | 150ms | transition all |