Componentes
Menus & seleção
Menu de ação
<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
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
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
| 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, e é preciso BUSCAR pra achar | Picker em diálogo — nunca um campo de busca dentro do menu (ver abaixo) |
| Ligar/desligar | Checkbox — não é escolha entre opções |
Quando é preciso buscar pra achar
| A escolha é | Peça | Molde |
|---|---|---|
| Entre poucas opções, dentro de um formulário | SelectField (select nativo) | No celular abre a roleta do sistema; funciona no teclado sozinho e não custa JS. |
| Entre muitas, e achar já é metade da tarefa | Dialog com busca dentro (picker) | ajustes/precificacao/insumo-picker-dialog.tsx — buscar → tocar no item → informar a quantidade → adicionar. |
| Sobre a lista inteira da tela | SearchInput na toolbar + chips de recorte | Filtra o que já está na tela; não escolhe um valor pra um campo. |
NuncaCampo de busca dentro de menu: recusado — 23/08/2026
O “SearchSelect” (menu flutuante com um campo de busca no topo) esteve desenhado neste guia e não vai existir. O motivo está escrito no código do picker que o substituiu: a versão anterior dele era um campo com datalist e “parecia campo de login — o 1Password pulava em cima”.
Campo de texto sem rótulo dentro de um painel flutuante é exatamente a assinatura que um gestor de senha procura. O picker em diálogo desarma isso: o valor escolhido viaja como uuid num campo escondido, e a busca leva data-1p-ignore + autoComplete="off" por garantia.
Teclado e acessibilidade
| 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”.