Skip to Content
ComponentsOrganismsDropdown menu

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

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

  1. Ação principal não deve ficar escondida no menu.
  2. Itens destrutivos ficam separados e nomeados claramente.
  3. Dropdown não substitui Select em formulário.
  4. Atalhos exibidos precisam existir.
  5. Em mobile, considere Drawer/Responsive Sheet se o menu ficar denso.
Last updated on