Checkbox

Pronto

Marca 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

Indeterminate (parent com filhos parcialmente marcados)
Lista de opções

Estados

Unchecked
Checked
Indeterminate
Hover (passe o mouse)
Focus (Tab)
Disabled (unchecked)
Disabled (checked)

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Check pop-incheckBackground fade ink, ícone ✓ aparece com scale 0→1200ms cubic-bezier(0.32,0.72,0,1)
Indeterminateindeterminate=trueMostra ícone Minus (−) no lugar do check150ms ease
Hover bordermouseenterBorda border → borderStrong150ms ease
Focus ringfocus-visibleOutline 2px borderInk com offset 2pxinstant
Click ripple (sutil)clickScale 0.9 no contêiner por 100ms100ms

Acessibilidade

Acessibilidade — checklist

Teclado
TabMove foco para o checkbox
SpaceMarca/desmarca
Shift+TabMove 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

TokenValorPapel
T.ink#0A0A0Abackground quando checked
T.surface#FFFFFFbackground unchecked + ícone ✓
T.border#E3DFD2borda unchecked
T.ink2#1A1A1Ccor do label
RADIUS.sm6borderRadius
MOTION.fast150mstransition all