Radio

Pronto

Marca 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

Lista vertical (default)
Lista horizontal (max 3 opções curtas)
Cards (com descrição)

Estados

Unselected
Selected
Hover
Focus (Tab)
Disabled
Disabled selected

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Dot pop-inselectCírculo interno scale 0→1 + opacity 0→1200ms cubic-bezier(0.32,0.72,0,1)
Border darkenselectBorda passa de border para borderInk150ms ease
Focus ringfocus-visibleOutline 2px borderInk com offset 2pxinstant
Arrow navArrowDown/Up no groupMove seleção para próximo/anteriorinstant

Acessibilidade

Acessibilidade — checklist

Teclado
TabMove foco para o radio group (entra no item selecionado)
↑ / ↓Move seleção entre opções
SpaceSeleciona 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

TokenValorPapel
T.ink#0A0A0Aborda + dot quando selected
T.surface#FFFFFFbackground
T.border#E3DFD2borda unselected
RADIUS.pill980forma circular
TYPE.base13fontSize do label