Dropdown Menu
Organism
Dropdown Menu é para ações ou opções contextuais. Não é Select de formulário.
Use quando um trigger precisa revelar ações secundárias, overflow, seções de menu, atalhos ou ações de linha. A ação principal não deve ficar escondida no menu.
Exemplos copiáveis
React
React
import * as React from "react"
type DropdownItem = {
id: string
label: string
shortcut?: string
destructive?: boolean
disabled?: boolean
onSelect: () => void
}
type DropdownSection = {
label?: string
items: DropdownItem[]
}
type PulsoDropdownMenuProps = {
label: string
sections: DropdownSection[]
align?: "start" | "end"
}
export function PulsoDropdownMenu({ label, sections, align = "end" }: PulsoDropdownMenuProps) {
const [open, setOpen] = React.useState(false)
const menuId = React.useId()
const menuRef = React.useRef<HTMLDivElement>(null)
React.useEffect(() => {
if (!open) return
function onPointerDown(event: PointerEvent) {
if (!menuRef.current?.contains(event.target as Node)) setOpen(false)
}
function onKeyDown(event: KeyboardEvent) {
if (event.key === "Escape") setOpen(false)
}
document.addEventListener("pointerdown", onPointerDown)
document.addEventListener("keydown", onKeyDown)
return () => {
document.removeEventListener("pointerdown", onPointerDown)
document.removeEventListener("keydown", onKeyDown)
}
}, [open])
return (
<div ref={menuRef} className="relative inline-block text-left">
<button type="button" aria-haspopup="menu" aria-expanded={open} aria-controls={menuId} onClick={() => setOpen((value) => !value)} className="inline-flex min-h-9 items-center gap-2 rounded-[var(--radius-md)] border border-[var(--border)] bg-[var(--card)] px-3 text-sm font-medium text-[var(--fg)] hover:bg-[var(--muted)] focus-visible:outline focus-visible:outline-[var(--ring-width)] focus-visible:outline-offset-2 focus-visible:outline-[var(--ring)]">
{label}<span aria-hidden="true">▾</span>
</button>
{open ? (
<div id={menuId} role="menu" className={["absolute z-50 mt-2 w-56 rounded-[var(--radius-lg)] border border-[var(--border)] bg-[var(--popover)] p-1 shadow-[var(--shadow-lg)]", align === "end" ? "right-0" : "left-0"].join(" ")}>
{sections.map((section, sectionIndex) => (
<div key={section.label ?? sectionIndex} className="not-first:mt-1 not-first:border-t not-first:border-[var(--border)] not-first:pt-1">
{section.label ? <p className="px-2 py-1.5 font-mono text-[10.5px] uppercase tracking-[var(--tracking-kicker)] text-[var(--fg-subtle)]">{section.label}</p> : null}
{section.items.map((item) => (
<button key={item.id} type="button" role="menuitem" disabled={item.disabled} onClick={() => { item.onSelect(); setOpen(false) }} className={["flex w-full items-center justify-between gap-3 rounded-[var(--radius-sm)] px-2 py-2 text-left text-sm", item.destructive ? "text-[var(--risk-critical-text)] hover:bg-[var(--risk-critical-bg)]" : "text-[var(--fg)] hover:bg-[var(--muted)]", "disabled:cursor-not-allowed disabled:text-[var(--fg-disabled)]"].join(" ")}>
<span>{item.label}</span>
{item.shortcut ? <kbd className="font-mono text-[11px] text-[var(--fg-muted)]">{item.shortcut}</kbd> : null}
</button>
))}
</div>
))}
</div>
) : null}
</div>
)
}<PulsoDropdownMenu
label="Ações"
sections={[
{ label: "Menção", items: [{ id: "assign", label: "Atribuir", shortcut: "A", onSelect: () => {} }] },
{ items: [{ id: "archive", label: "Arquivar menção", destructive: true, onSelect: () => {} }] },
]}
/>Regras
- Ação principal não deve ficar escondida no menu.
- Itens destrutivos ficam separados e nomeados claramente.
- Dropdown não substitui Select em formulário.
- Atalhos exibidos precisam existir.
- Em mobile, considere Drawer/Responsive Sheet se o menu ficar denso.
Last updated on