Select

Pronto

Escolhe 1 de N opções fixas via dropdown. Ideal para 5-50 opções; menos use Radio, mais use Combobox com busca.

Usar quando

Lista fechada de opções (Estado, Tipo de evento, Status, Plano). Quando opções não cabem inline.

Não usar quando

2-5 opções (use Radio). Lista enorme ou com busca (use Combobox). Múltipla seleção (use Combobox multi).

Variantes

Estados

Closed (default)
Open
  • São Paulo
  • Rio de Janeiro
  • Minas Gerais
  • Rio Grande do Sul
  • Paraná
Disabled
Error

Selecione uma opção.

Microinterações

MicrointeraçãoDisparada porComportamentoTiming
Open dropdownclick no triggerDropdown desce com fade + translateY 4→0200ms cubic-bezier(0.32,0.72,0,1)
Chevron rotateopen/closeChevron rota 0deg → 180deg200ms ease
Item hovermouseenter no itemBackground → surfaceHover100ms ease
Selected checkitem selecionadoCheck ✓ aparece à direita do iteminstant
Click outsideclick fora do dropdownFecha com fade-out150ms ease
Keyboard navArrowDown/UpMove highlight, scroll automático se fora da viewinstant
Type-aheadtecla letraPula para primeira opção começando com a letrainstant

Acessibilidade

Acessibilidade — checklist

Teclado
TabMove foco para o select
Space / EnterAbre/fecha dropdown
↓ / ↑Navega opções
EnterSeleciona opção em foco
EscFecha dropdown sem selecionar
A-Z / 0-9Type-ahead — pula para primeira opção com a letra
Home / EndVai para primeira / última opção
ARIA esperado
  • role="combobox" aria-haspopup="listbox" aria-expanded="true|false"
  • <ul role="listbox"> com <li role="option" aria-selected="...">
  • aria-activedescendant="opt-id" indica item em highlight
  • aria-controls="listbox-id"
  • <label htmlFor="select-id">
Notas
  • Tecla letra faz "type-ahead" (pula opção pela inicial) — comportamento esperado.
  • Esc fecha sem selecionar; Enter confirma.
  • Em mobile, native <select> é melhor (nativo do OS).
  • Listbox aberto deve manter foco com aria-activedescendant.

Código

'use client';
import { useState } from 'react';
import { ChevronDown } from 'lucide-react';
import { T, TYPE, SP, RADIUS, transition } from '@/lib/tokens';

interface Option { value: string; label: string }
interface SelectProps {
  value: string;
  onChange: (v: string) => void;
  options: Option[];
  placeholder?: string;
}

export function Select({ value, onChange, options, placeholder }: SelectProps) {
  const [open, setOpen] = useState(false);
  const selected = options.find(o => o.value === value);
  return (
    <div style={{ position: 'relative' }}>
      <button type="button" role="combobox" aria-haspopup="listbox" aria-expanded={open}
        onClick={() => setOpen(!open)}
        style={{
          width: '100%', height: 36,
          padding: `0 ${SP[3]}px`,
          background: T.surface,
          border: `1px solid ${open ? T.borderInk : T.border}`,
          borderRadius: RADIUS.md,
          fontSize: TYPE.base, color: selected ? T.ink : T.inkSubtle,
          textAlign: 'left',
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          transition: transition('border-color', 'fast'),
        }}>
        <span>{selected?.label ?? placeholder}</span>
        <ChevronDown size={14} style={{ transform: open ? 'rotate(180deg)' : 'none', transition: transition('transform', 'fast') }} />
      </button>
      {open && (
        <ul role="listbox" style={{ position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 4,
          background: T.surface, border: `1px solid ${T.border}`, borderRadius: RADIUS.md,
          boxShadow: T.shadowMd, padding: SP[1], maxHeight: 240, overflowY: 'auto', zIndex: 10 }}>
          {options.map(o => (
            <li key={o.value} role="option" aria-selected={o.value === value}
              onClick={() => { onChange(o.value); setOpen(false); }}
              style={{ padding: `${SP[2]}px ${SP[2]}px`, borderRadius: RADIUS.sm, cursor: 'pointer',
                fontSize: TYPE.base, color: T.ink2 }}>
              {o.label}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

Regras

Faça

  • Use para 5-50 opções fixas.
  • Sempre tenha placeholder se não houver default.
  • Mostre ✓ no item selecionado quando dropdown aberto.
  • Suporte navegação por teclado (↑↓ Enter Esc Home End).
  • Suporte type-ahead (tecla letra pula).
  • Em mobile, considere <select> nativo.

Não faça

  • Não use para 2-5 opções (use Radio).
  • Não use para >50 opções (use Combobox com busca).
  • Não bloqueie clicks fora — eles devem fechar dropdown.
  • Não esconda placeholder após seleção (mostre o valor).
  • Não force submit ao selecionar (deixe usuário confirmar).

Tokens usados

TokenValorPapel
T.surface#FFFFFFbackground do trigger e dropdown
T.border#E3DFD2borda default
T.borderInk#000000borda quando open
T.surfaceHover#F7F5EChover de item
T.shadowMd0 4px 14px ...sombra do dropdown
RADIUS.md12borderRadius
TYPE.base13fontSize