Componentes
Menus & seleção
Uma pergunta decide tudo aqui: escolher ou agir? Escolher 1 entre N pinta o escolhido de verde e mantém a lista viva; agir só destaca no hover e fecha o menu. Mesma peça técnica, duas gramáticas — trocá-las faz a pessoa achar que desfez uma escolha quando na verdade executou uma ação.
src/components/ui/dropdown-menu.tsxradix-uianimate-popup-in
Menu de ação
O kebab de uma linha, o “mais” de uma toolbar. Ninguém fica selecionado depois — o menu executa e fecha.
← kebab de linha · menu da toolbar
o uso
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon-sm" aria-label="Ações de Marina Alvez">
<EllipsisVertical />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onSelect={editar}><Pencil /> Editar cadastro</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onSelect={excluir}>
<Trash2 /> Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
| DropdownMenuContent sideOffset | number | 6 | A distância do painel até o gatilho. 6px é o respiro do canon; não mexer sem motivo. |
| DropdownMenuContent align | "start" | "center" | "end" | "center" | Menu de linha alinha por start; menu de toolbar à direita alinha por end, pra não vazar da tela. |
| DropdownMenuItem variant | "default" | "destructive" | "default" | O destrutivo pinta texto e ícone de vermelho e usa focus:bg-destructive/10. |
| DropdownMenuItem onSelect | (e) => void | — | Preferir a onClick: cobre teclado e Enter, e o Radix fecha o menu sozinho. |
| Peça | Receita |
|---|---|
| Painel | rounded-xl border bg-surface p-1 shadow-md ring-1 ring-foreground/5 |
| Entrada | animate-popup-in (100ms) com origin vindo do Radix — o zoom nasce do canto por onde abriu |
| Saída | seca, sem animação |
| Item | rounded-md px-2 py-1.5 text-sm · ícone size-4 em fg-subtle |
| Item em foco | focus:bg-ink-50 (é o hover E o teclado — o Radix usa focus) |
| Separador | my-1 h-px bg-border |
| Largura mínima | min-w-[8rem] |
Armadilha já pagaFechar por código não avisa
Fechar um Radix
Dialog ou menu programaticamente NÃO dispara onOpenChange — o estado da última abertura vaza pra próxima. Remonte com uma key por abertura quando o conteúdo depender do que estava aberto.Escolher 1 entre N
Filtro, ordenação, troca de visão. O escolhido fica verde e a contagem mora à direita — a lista continua fazendo sentido depois de fechada.
← o item escolhido pinta de verde
Sim
✓ Odontologia 34
Dermatologia 27
Escolha: o item ativo é verde e permanece. A contagem alinha à direita, tabular.
Não
Excluir paciente
Ver histórico
Ação pintada de verde parece escolha feita: quem lê acha que “Excluir paciente” é o estado atual, não o que vai acontecer no clique.
Quando NÃO usar menu pra escolher
Nem toda escolha merece painel flutuante.
| Situação | Use |
|---|---|
| Escolha dentro de um formulário | SelectField (select nativo) — no celular ele abre a roleta do sistema |
| Duas ou três opções, sempre visíveis | Grupo de pílulas com aria-pressed, ou abas se forem LUGARES |
| Muitas opções com busca | Menu com um SearchInput no topo (o padrão “SearchSelect”) |
| Ligar/desligar | Checkbox — não é escolha entre opções |
NotaAberto: o “SearchSelect” ainda não existe
O “escolher 1 entre N com busca dentro do menu” está desenhado aqui, mas não tem instância no app nem componente em
ui/: as listas longas de hoje filtram pela busca da TELA, não por um campo dentro do painel. Se a sua precisa do campo dentro, componha (DropdownMenu + SearchInput + a gramática de item acima) e avise — a segunda ocorrência é o gatilho pra virar primitivo.Teclado e acessibilidade
O Radix entrega quase tudo; o que sobra é não estragar.
| Tecla | O que faz |
|---|---|
| Enter / Espaço no gatilho | Abre e põe o foco no primeiro item |
| ↑ ↓ | Anda pelos itens (o item em foco usa focus:bg-ink-50) |
| Digitar uma letra | Pula pro item que começa com ela |
| Esc | Fecha e devolve o foco pro gatilho |
| Tab | Fecha o menu e segue o fluxo da página |
O gatilho usa asChild pra virar o Button do canon — assim ele herda a pílula, o anel de foco e o crescimento no dedo, sem aninhar botão dentro de botão. Gatilho só de ícone precisa de aria-label com o objeto: “Ações de Marina Alvez”, não “Ações”.