icon

Morainev0.5.0

DropdownMenu
overlaysdropdown-menu

DropdownMenu

Triggered action menu anchored to its child content.

Import#

import { DropdownMenu } from 'moraine'

Slot Structure#

Wrapper trigger with a floating menu portal containing grouped items.

Use contentProps and itemProps to forward native attributes, refs, and event handlers. Item handlers run before built-in menu behavior and can call preventDefault() to cancel activation.

trigger
└── content (portal)
└── group (×n)
├── label (optional)
├── separator (optional)
└── item (×n)

Item internals#

item
├── itemLeading (optional)
├── itemWrapper
│ ├── itemLabel (optional)
│ └── itemDescription (optional)
└── itemTrailing
└── itemIndicator (optional, checkbox items)

Examples#

Sizes#

Menu item size scale from sm to lg for compact and roomy density.

function Sizes() {
const SIZES = ['sm', 'md', 'lg'] as const
const ITEMS: DropdownMenuT.Item[] = [
{
type: 'group',
children: [
{
label: 'Profile',
icon: 'i-lucide-user',
},
{
label: 'Settings',
icon: 'i-lucide-settings-2',
},
{
label: 'Sign Out',
icon: 'i-lucide-log-out',
color: 'destructive',
},
],
},
]
return (
<div class="flex flex-wrap gap-3">
<For each={SIZES}>
{(size) => (
<DropdownMenu size={size} items={ITEMS}>
<Button variant="outline" size="sm">
{size} Menu
</Button>
</DropdownMenu>
)}
</For>
</div>
)
}

Account / Team#

An account dropdown with grouped actions, workspace switching, shortcut hints, and a destructive sign-out row.

function AccountTeam() {
const badgeClass =
'rounded-md b-1 b-border border-border bg-muted px-1.5 py-0.5 font-medium text-[11px] text-foreground'
const avatarClass =
'grid size-4 place-items-center rounded-full bg-linear-to-br from-primary to-accent text-[10px] font-semibold text-primary-foreground'
const [lastAction, setLastAction] = createSignal('None')
return (
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<DropdownMenu
items={[
{
type: 'group',
label: 'Account',
children: [
{
label: (
<div class="flex gap-2 items-center">
<span class="font-medium">Alex Morgan</span>
<span class={badgeClass}>Owner</span>
</div>
),
description: '[email protected]',
icon: <span class={avatarClass}>AM</span>,
onSelect: () => setLastAction('Open account profile'),
},
{ type: 'separator' },
{
label: 'Switch Workspace',
icon: 'i-lucide-building-2',
children: [
{
type: 'group',
label: 'Recent Workspaces',
children: [
{
label: 'Design System',
description: '12 teammates · shared tokens',
icon: 'i-lucide-palette',
onSelect: () => setLastAction('Switch to Design System'),
},
{
label: 'Platform Ops',
description: '8 teammates · deploy tooling',
icon: 'i-lucide-server',
onSelect: () => setLastAction('Switch to Platform Ops'),
},
{
label: 'Support Workspace',
description: '5 teammates · customer issues',
icon: 'i-lucide-life-buoy',
onSelect: () => setLastAction('Switch to Support Workspace'),
},
],
},
{
type: 'group',
label: 'Actions',
children: [
{
label: 'Create Workspace',
icon: 'i-lucide-plus',
onSelect: () => setLastAction('Create workspace'),
},
],
},
],
},
{
label: 'Invite Teammates',
icon: 'i-lucide-user-plus',
kbds: ['⌘', 'I'],
onSelect: () => setLastAction('Invite teammates'),
},
{
label: 'Billing & Usage',
icon: 'i-lucide-credit-card',
onSelect: () => setLastAction('Billing & usage'),
},
],
},
{
type: 'group',
label: 'Preferences',
children: [
{
label: 'Account Settings',
icon: 'i-lucide-settings-2',
kbds: ['⌘', ','],
onSelect: () => setLastAction('Account settings'),
},
{
label: 'Keyboard Shortcuts',
icon: 'i-lucide-command',
kbds: ['⌘', 'K'],
onSelect: () => setLastAction('Keyboard shortcuts'),
},
{
label: 'Support Inbox',
icon: 'i-lucide-life-buoy',
onSelect: () => setLastAction('Support inbox'),
},
],
},
{
type: 'group',
children: [
{
label: 'Sign Out',
icon: 'i-lucide-log-out',
color: 'destructive',
onSelect: () => setLastAction('Sign out'),
},
],
},
]}
>
<Button variant="outline">Open account menu</Button>
</DropdownMenu>
<p class="text-sm text-muted-foreground">
Last action: <span class="font-medium">{lastAction()}</span>
</p>
</div>
)
}

Editor / View Options#

A workspace-style menu with recent files, nested submenus, checkbox toggles, and theme selection for keyboard and pointer testing.

function EditorViewOptions() {
const [showLineNumbers, setShowLineNumbers] = createSignal(true)
const [showMinimap, setShowMinimap] = createSignal(true)
const [previewTabs, setPreviewTabs] = createSignal(false)
const [autoSave, setAutoSave] = createSignal(true)
const [theme, setTheme] = createSignal<'light' | 'dark' | 'system'>('dark')
const editorItems = createMemo<DropdownMenuT.Item[]>(() => [
{
type: 'group',
label: 'Editor',
children: [
{
label: 'Command Palette',
description: 'Jump to commands, files, and symbols',
icon: 'i-lucide-search',
kbds: ['⌘', 'K'],
},
{
label: 'Go to File…',
icon: 'i-lucide-file-search',
kbds: ['⌘', 'P'],
},
{
label: 'Open Recent',
icon: 'i-lucide-history',
children: [
{
type: 'group',
label: 'Recent Files',
children: [
{
label: 'src/overlays/dropdown-menu/dropdown-menu.tsx',
icon: 'i-lucide-file-code-2',
},
{
label: 'docs/components/context-menu-demos.tsx',
icon: 'i-lucide-file-code-2',
},
{
label: 'Pinned Workspaces',
icon: 'i-lucide-star',
children: [
{
type: 'group',
children: [
{
label: 'Moraine',
description: 'packages + docs',
icon: 'i-lucide-folder-kanban',
},
{
label: 'Docs Site',
description: 'marketing + guides',
icon: 'i-lucide-book-open',
},
],
},
],
},
],
},
],
},
],
},
{
type: 'group',
label: 'View',
children: [
{
type: 'checkbox',
label: 'Line Numbers',
icon: 'i-lucide-list',
checked: showLineNumbers(),
onCheckedChange: (checked: boolean) => setShowLineNumbers(checked),
},
{
type: 'checkbox',
label: 'Minimap',
icon: 'i-lucide-map',
checked: showMinimap(),
onCheckedChange: (checked: boolean) => setShowMinimap(checked),
},
{
type: 'checkbox',
label: 'Preview Tabs',
icon: 'i-lucide-panel-top',
checked: previewTabs(),
onCheckedChange: (checked: boolean) => setPreviewTabs(checked),
},
{
type: 'checkbox',
label: 'Auto Save',
icon: 'i-lucide-save',
checked: autoSave(),
onCheckedChange: (checked: boolean) => setAutoSave(checked),
},
{ type: 'separator' },
{
label: 'Theme',
icon: 'i-lucide-palette',
children: [
{
type: 'group',
label: 'Appearance',
children: [
{
label: 'Light',
icon: theme() === 'light' ? 'i-lucide-check' : 'i-lucide-sun',
onSelect: () => setTheme('light'),
},
{
label: 'Dark',
icon: theme() === 'dark' ? 'i-lucide-check' : 'i-lucide-moon',
onSelect: () => setTheme('dark'),
},
{
label: 'System',
icon: theme() === 'system' ? 'i-lucide-check' : 'i-lucide-monitor',
onSelect: () => setTheme('system'),
},
],
},
],
},
],
},
{
type: 'group',
label: 'Panels',
children: [
{
label: 'Toggle Terminal',
icon: 'i-lucide-square-terminal',
kbds: ['⌃', '`'],
},
{
label: 'Focus Problems',
icon: 'i-lucide-triangle-alert',
kbds: ['⇧', '⌘', 'M'],
},
],
},
])
return (
<div class="flex flex-col gap-3">
<div class="flex flex-wrap gap-3 items-center">
<DropdownMenu items={editorItems()}>
<Button variant="outline">Editor menu</Button>
</DropdownMenu>
</div>
<div class="text-sm text-muted-foreground flex flex-wrap gap-4">
<span>
Line numbers: <span class="font-medium">{String(showLineNumbers())}</span>
</span>
<span>
Minimap: <span class="font-medium">{String(showMinimap())}</span>
</span>
<span>
Preview tabs: <span class="font-medium">{String(previewTabs())}</span>
</span>
<span>
Auto save: <span class="font-medium">{String(autoSave())}</span>
</span>
<span>
Theme: <span class="font-medium uppercase">{theme()}</span>
</span>
</div>
</div>
)
}

Project / Release Actions#

A heavier project menu with move flows, release actions, mixed-content labels, and destructive project operations.

function ProjectReleaseActions() {
const [lastAction, setLastAction] = createSignal('None')
const badgeClass =
'rounded-md b-1 b-border border-border bg-muted px-1.5 py-0.5 font-medium text-[11px] text-foreground'
const projectItems: DropdownMenuT.Item[] = [
{
type: 'group',
label: (
<div class="flex gap-2 items-center">
<span>Project: moraine</span>
<span class={badgeClass}>main</span>
</div>
),
children: [
{
label: 'New File',
icon: 'i-lucide-file-plus',
kbds: ['⌘', 'N'],
onSelect: () => setLastAction('New file'),
},
{
label: 'New Folder',
icon: 'i-lucide-folder-plus',
kbds: ['⇧', '⌘', 'N'],
onSelect: () => setLastAction('New folder'),
},
{
label: 'Rename Project',
icon: 'i-lucide-pencil',
kbds: ['F2'],
onSelect: () => setLastAction('Rename project'),
},
{ type: 'separator' },
{
label: 'Move To…',
icon: 'i-lucide-folder-input',
children: [
{
type: 'group',
label: 'Favorite Folders',
children: [
{
label: 'src/components',
icon: 'i-lucide-folder-open',
onSelect: () => setLastAction('Move to src/components'),
},
{
label: 'src/overlays',
icon: 'i-lucide-folder-open',
onSelect: () => setLastAction('Move to src/overlays'),
},
{
label: 'More Destinations',
icon: 'i-lucide-more-horizontal',
children: [
{
type: 'group',
children: [
{
label: 'docs/content',
icon: 'i-lucide-folder-open',
onSelect: () => setLastAction('Move to docs/content'),
},
{
label: 'archive/2025',
icon: 'i-lucide-folder-open',
onSelect: () => setLastAction('Move to archive/2025'),
},
],
},
],
},
],
},
],
},
{
label: 'Copy Preview Link',
icon: 'i-lucide-link',
kbds: ['⌘', '⇧', 'C'],
onSelect: () => setLastAction('Copy preview link'),
},
],
},
{
type: 'group',
label: 'Release',
children: [
{
label: 'Open Pull Request',
icon: 'i-lucide-git-pull-request-arrow',
onSelect: () => setLastAction('Open pull request'),
},
{
label: 'Deploy Preview',
description: 'Build preview for design review',
icon: 'i-lucide-rocket',
onSelect: () => setLastAction('Deploy preview'),
},
{
label: (
<div class="flex gap-2 items-center">
<span>Ship to Production</span>
<span class="text-[11px] text-amber-700 font-medium px-1.5 py-0.5 rounded-md bg-amber-100">
Protected
</span>
</div>
),
description: 'Requires review approval',
icon: <span class="rounded-full bg-emerald-500 size-2 inline-block" />,
onSelect: () => setLastAction('Ship to production'),
},
],
},
{
type: 'group',
children: [
{
label: 'Archive Project',
icon: 'i-lucide-archive',
onSelect: () => setLastAction('Archive project'),
},
{
label: 'Delete Project',
icon: 'i-lucide-trash-2',
color: 'destructive',
onSelect: () => setLastAction('Delete Project'),
},
],
},
]
return (
<>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<DropdownMenu items={projectItems}>
<Button>Project actions</Button>
</DropdownMenu>
<p class="text-sm text-muted-foreground">
Tip: use arrow keys to walk the nested “Move To…” and release sections.
</p>
</div>
<div class="text-sm text-muted-foreground">
Last action: <span class="font-medium">{lastAction()}</span>
</div>
</>
)
}

API Reference#

Attributes#

Slottrigger6 attributes
Element users activate to open the menu.

Data Attributes

3
Data AttributeDescription
data-closed
State or slot attribute exposed for styling hooks and selectors.
data-disabled
Present when the component or item is disabled.
data-expanded
State or slot attribute exposed for styling hooks and selectors.

ARIA Attributes

3
ARIA AttributeDescription
aria-controls
References the controlled element while the related content is mounted.
aria-expanded
Indicates whether the controlled content is expanded.
aria-haspopup
Accessibility attribute forwarded by the rendered component.

Props#

PropTypeDefaultDescription
checkedIconIconT.Nameicon-check
Icon used for checked checkbox items.
childrenJSX.Element
Trigger content used to open the dropdown menu.
classClassValue
Class applied to the component root or trigger element.
classes(OverlayMenuSharedClasses & DropdownMenuT.Classes) | undefined
Slot class overrides for menu sections.
contentBottomOverlayMenuContentSlot | undefined
Content rendered after the resolved item groups.
contentPropsElementProps<HTMLDivElement> | undefined
Additional attributes for each menu layer content element.
contentTopOverlayMenuContentSlot | undefined
Content rendered before the resolved item groups.
defaultOpenboolean | undefinedfalse
Initial open state when the component is uncontrolled.
disabledboolean | undefinedfalse
Whether trigger interactions should be ignored.
gutternumber | undefined0
Gap between the anchor and the content.
idstring | undefined
Unique base id used to derive trigger and content ids.
itemProps((props: DropdownMenuT.ItemRenderProps) => ElementProps<HTMLDivElement> | undefined) | undefined
Additional attributes for an interactive menu item.
itemRenderComponentOrElement<DropdownMenuT.ItemRenderProps> | undefined
Custom renderer for individual items.
itemsDropdownMenuT.Item[] | undefined
Items rendered in the menu body.
onClickJSX.EventHandlerUnion<HTMLSpanElement, MouseEvent> | undefined
Root trigger click handler.
onKeyDownJSX.EventHandlerUnion<HTMLSpanElement, KeyboardEvent> | undefined
Root trigger keyboard handler.
onOpenChange((open: boolean) => void) | undefined
Called whenever the menu requests an open state change.
openboolean | undefined
Controlled open state of the menu.
overflowPaddingnumber | undefined4
Padding applied to the overflow area when calculating the menu's position.
placementOverlayMenuPlacement | undefined
Preferred content placement relative to the trigger or anchor point.
preventScrollboolean | undefinedtrue
Whether body scroll should be locked while the menu is open.
refJSX.HTMLElementTags["span"] extends { ref?: infer Ref; } ? Ref : never | undefined
size"sm" | "md" | "lg" | undefinedmd
Menu item size variant.
styleJSX.CSSProperties | undefined
styles(OverlayMenuSharedStyles & DropdownMenuT.Styles) | undefined
Slot style overrides for menu sections.
submenuIconIconT.Nameicon-chevron-right
Icon used for submenu trigger items.

Items#

PropTypeDefaultDescription
checkedboolean | undefined
Controlled checked state for checkbox and radio items.
childrenDropdownMenuT.Item[] | undefined
Nested menu items for creating submenus.
colorNonNullable<"default" | "destructive" | undefined> | undefined
Color theme variant for the menu item.
defaultCheckedboolean | undefined
Initial checked state for uncontrolled checkbox and radio items.
defaultOpenboolean | undefined
Initial open state for submenus.
descriptionJSX.Element
Secondary description text displayed below the label.
disabledboolean | undefinedfalse
Whether the item is non-interactive.
groupstring | undefined
Radio group identifier. Radio items with the same group are mutually exclusive.
iconIconT.Name
Icon name or custom element to display at the start of the item.
kbdsstring[] | undefined
Array of keyboard shortcuts to display as keys.
labelJSX.Element
Primary label text or element.
onCheckedChange((checked: boolean) => void) | undefined
Event handler called when a checkbox item's state changes.
onSelect(() => void) | undefined
Event handler called when the item is activated.
onValueChange((value: string) => void) | undefined
Event handler called when a radio item is selected.
openboolean | undefined
Controlled open state for submenus.
typeOverlayMenuItemType | undefineditem
The type of menu item to render.
valuestring | undefined
Radio item value reported by onValueChange and used for grouped selection.