Select
ProntoEscolhe 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
Microinterações
| Microinteração | Disparada por | Comportamento | Timing |
|---|---|---|---|
| Open dropdown | click no trigger | Dropdown desce com fade + translateY 4→0 | 200ms cubic-bezier(0.32,0.72,0,1) |
| Chevron rotate | open/close | Chevron rota 0deg → 180deg | 200ms ease |
| Item hover | mouseenter no item | Background → surfaceHover | 100ms ease |
| Selected check | item selecionado | Check ✓ aparece à direita do item | instant |
| Click outside | click fora do dropdown | Fecha com fade-out | 150ms ease |
| Keyboard nav | ArrowDown/Up | Move highlight, scroll automático se fora da view | instant |
| Type-ahead | tecla letra | Pula para primeira opção começando com a letra | instant |
Acessibilidade
Acessibilidade — checklist
Teclado
| Tab | Move foco para o select |
| Space / Enter | Abre/fecha dropdown |
| ↓ / ↑ | Navega opções |
| Enter | Seleciona opção em foco |
| Esc | Fecha dropdown sem selecionar |
| A-Z / 0-9 | Type-ahead — pula para primeira opção com a letra |
| Home / End | Vai 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
| Token | Valor | Papel |
|---|---|---|
T.surface | #FFFFFF | background do trigger e dropdown |
T.border | #E3DFD2 | borda default |
T.borderInk | #000000 | borda quando open |
T.surfaceHover | #F7F5EC | hover de item |
T.shadowMd | 0 4px 14px ... | sombra do dropdown |
RADIUS.md | 12 | borderRadius |
TYPE.base | 13 | fontSize |