Radio
ProntoMarca exatamente 1 de N opções fixas. Use para 2-5 opções; mais que isso, use Select.
Usar quando
Escolha exclusiva entre poucas opções (até 5). Configurações binárias com texto descritivo. Filtros mutuamente excludentes.
Não usar quando
Mais de 5 opções (use Select). Múltipla seleção (use Checkbox). Liga/desliga (use Toggle).
Variantes
Estados
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Dot pop-in | select | Círculo interno scale 0→1 + opacity 0→1 | 200ms cubic-bezier(0.32,0.72,0,1) |
| Border darken | select | Borda passa de border para borderInk | 150ms ease |
| Focus ring | focus-visible | Outline 2px borderInk com offset 2px | instant |
| Arrow nav | ArrowDown/Up no group | Move seleção para próximo/anterior | instant |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Move foco para o radio group (entra no item selecionado) |
| ↑ / ↓ | Move seleção entre opções |
| Space | Seleciona se nenhum estiver selecionado |
ARIA esperado
- <fieldset><legend>Pergunta</legend>...</fieldset>
- role="radiogroup"
- <input type="radio" name="x" /> agrupa por mesmo name
- aria-checked="true" no selecionado
Notas
- Sempre agrupe radios pelo mesmo atributo name (HTML behavior).
- Use <fieldset><legend> para agrupar visualmente E semanticamente.
- Setas ↑↓ navegam o grupo (browser nativo); Tab move pra fora.
- Default selection: ou nenhum, ou o "mais comum" — nunca arbitrário.
Código
'use client';
import { T, TYPE, SP, MOTION, transition } from '@/lib/tokens';
interface RadioProps {
name: string;
value: string;
checked: boolean;
onChange: (v: string) => void;
label: string;
disabled?: boolean;
}
export function Radio({ name, value, checked, onChange, label, disabled }: RadioProps) {
return (
<label style={{ display: 'inline-flex', alignItems: 'center', gap: SP[2], cursor: disabled ? 'not-allowed' : 'pointer' }}>
<input type="radio" name={name} value={value} checked={checked}
onChange={(e) => onChange(e.target.value)} disabled={disabled}
style={{ position: 'absolute', opacity: 0 }} />
<span style={{
width: 18, height: 18, borderRadius: 980,
background: T.surface,
border: `1.5px solid ${checked ? T.ink : T.border}`,
display: 'grid', placeItems: 'center',
transition: transition('border-color', 'fast'),
opacity: disabled ? 0.5 : 1,
}}>
{checked && <span style={{
width: 8, height: 8, borderRadius: 980, background: T.ink,
animation: 'lb-radio-pop 200ms cubic-bezier(0.32,0.72,0,1)',
}} />}
</span>
<span style={{ fontSize: TYPE.base, color: T.ink2 }}>{label}</span>
</label>
);
}Regras
Faça
- ✓Forma redonda (diferencia de Checkbox quadrado).
- ✓Mesma cor ink no border + dot quando selecionado.
- ✓Default: nenhum selecionado OU o "mais comum" (não arbitrário).
- ✓Setas ↑↓ navegam dentro do group.
- ✓Use radio cards quando opções têm descrição longa.
Não faça
- ✗Não use radio para múltipla seleção (use Checkbox).
- ✗Não use mais que 5 opções (use Select).
- ✗Não use radios soltos sem fieldset/legend agrupando.
- ✗Não use mesma name em radios independentes.
- ✗Não force seleção arbitrária como default.
Tokens usados
| Token | Valor | Papel |
|---|---|---|
T.ink | #0A0A0A | borda + dot quando selected |
T.surface | #FFFFFF | background |
T.border | #E3DFD2 | borda unselected |
RADIUS.pill | 980 | forma circular |
TYPE.base | 13 | fontSize do label |