DropdownMenu
A floating menu component with support for items, checkbox items, radio groups, and nested groups.
Demo
Features
- Full keyboard navigation
- Checkbox and radio item support
- Grouped items with labels
- Flexible positioning with collision detection
- Focus trapping in modal mode
- WCAG compliant with proper ARIA attributes
Installation
dotnet add package SummitUIAnatomy
Import the components and structure them as follows:
<SmDropdownMenuRoot>
<SmDropdownMenuTrigger class="su-dropdown-trigger">Open Menu</SmDropdownMenuTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuContent class="su-dropdown-content">
<SmDropdownMenuItem class="su-dropdown-item">Item 1</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item">Item 2</SmDropdownMenuItem>
<SmDropdownMenuSeparator class="su-dropdown-separator" />
<SmDropdownMenuItem class="su-dropdown-item">Item 3</SmDropdownMenuItem>
</SmDropdownMenuContent>
</SmDropdownMenuPortal>
</SmDropdownMenuRoot>Sub-components
DropdownMenuRoot
Root container managing menu state.
DropdownMenuTrigger
Button that toggles the menu.
DropdownMenuPortal
Renders content at document body level.
DropdownMenuContent
Floating content panel with positioning.
DropdownMenuItem
Single selectable menu item.
DropdownMenuCheckboxItem
Toggle-able checkbox menu item.
DropdownMenuRadioGroup
Container for radio items.
DropdownMenuRadioItem
Radio option within a group.
DropdownMenuSeparator
Visual separator between items.
DropdownMenuSub
Container for nested submenu.
DropdownMenuSubTrigger
Menu item that opens a submenu.
DropdownMenuSubContent
Floating content panel for submenu.
API Reference
DropdownMenuRoot
| Property | Type | Default | Description |
|---|---|---|---|
| Open | bool? | null | Controlled open state |
| DefaultOpen | bool | false | Default open state (uncontrolled) |
| OpenChanged | EventCallback<bool> | - | Callback when open state changes |
| OnOpen | EventCallback | - | Callback when menu opens |
| OnClose | EventCallback | - | Callback fired immediately when close is triggered (before animations start) |
| Modal | bool | true | Whether to trap focus |
DropdownMenuContent
| Property | Type | Default | Description |
|---|---|---|---|
| Side | Side | Bottom | Placement side |
| SideOffset | int | 4 | Offset from trigger (px) |
| Align | Align | Start | Alignment along side axis |
| AvoidCollisions | bool | true | Avoid viewport boundaries |
| Loop | bool | true | Loop keyboard navigation |
| OnOpenAutoFocus | EventCallback | - | Callback after menu opens and focus is set |
| OnCloseAutoFocus | EventCallback | - | Callback after close animations complete and focus returns to trigger |
DropdownMenuItem
| Property | Type | Default | Description |
|---|---|---|---|
| Disabled | bool | false | Disable item |
| OnSelect | EventCallback | - | Selection callback |
DropdownMenuSub
| Property | Type | Default | Description |
|---|---|---|---|
| Open | bool? | null | Controlled open state |
| DefaultOpen | bool | false | Default open state (uncontrolled) |
| OpenChanged | EventCallback<bool> | - | Callback when open state changes |
DropdownMenuSubTrigger
| Property | Type | Default | Description |
|---|---|---|---|
| Disabled | bool | false | Disable trigger |
| TextValue | string? | - | Text for typeahead search |
DropdownMenuSubContent
| Property | Type | Default | Description |
|---|---|---|---|
| SideOffset | int | 0 | Offset from trigger (px) |
| AlignOffset | int | 0 | Alignment offset (px) |
| AvoidCollisions | bool | true | Avoid viewport boundaries |
| Loop | bool | true | Loop keyboard navigation |
Callback Timing with Animations
When using CSS animations on menu content, understanding when each callback fires is important for coordinating state changes.
| Callback | Location | When it fires |
|---|---|---|
| OnClose | DropdownMenuRoot | Immediately when close is triggered (before animations) |
| OnCloseAutoFocus | DropdownMenuContent | After all close animations complete and focus returns to trigger |
When to use each
- OnClose: Use for immediate state updates, analytics tracking, or when animation timing doesn't matter
- OnCloseAutoFocus: Use when you need the menu to be fully closed (e.g., cleanup operations, navigation, showing toasts)
@* OnClose fires immediately when menu starts closing *@
@* OnCloseAutoFocus fires after animations complete *@
<SmDropdownMenuRoot OnClose="@HandleClose">
<SmDropdownMenuTrigger>Options</SmDropdownMenuTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuContent
class="animate-out fade-out-0 zoom-out-95 duration-150"
OnCloseAutoFocus="@HandleCloseComplete">
<SmDropdownMenuItem>Item 1</SmDropdownMenuItem>
<SmDropdownMenuItem>Item 2</SmDropdownMenuItem>
</SmDropdownMenuContent>
</SmDropdownMenuPortal>
</SmDropdownMenuRoot>
@code {
private void HandleClose()
{
// Fires immediately - use for analytics, state updates
Console.WriteLine("Menu closing...");
}
private void HandleCloseComplete()
{
// Fires after animation - use for cleanup, navigation
Console.WriteLine("Menu fully closed");
}
}Examples
Basic Menu
<SmDropdownMenuRoot>
<SmDropdownMenuTrigger class="su-dropdown-trigger">Options</SmDropdownMenuTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuContent class="su-dropdown-content" SideOffset="4">
<SmDropdownMenuItem class="su-dropdown-item" OnSelect="@(() => HandleAction("new"))">
New File
</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item" OnSelect="@(() => HandleAction("open"))">
Open File
</SmDropdownMenuItem>
<SmDropdownMenuSeparator class="su-dropdown-separator" />
<SmDropdownMenuItem class="su-dropdown-item" OnSelect="@(() => HandleAction("save"))">
Save
</SmDropdownMenuItem>
</SmDropdownMenuContent>
</SmDropdownMenuPortal>
</SmDropdownMenuRoot>With Checkbox Items
Toggle-able menu items.
@code {
private bool showToolbar = true;
private bool showSidebar = false;
}
<SmDropdownMenuRoot>
<SmDropdownMenuTrigger class="su-dropdown-trigger">View</SmDropdownMenuTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuContent class="su-dropdown-content">
<SmDropdownMenuCheckboxItem class="su-dropdown-item" @bind-Checked="showToolbar">
@(context.Checked ? "✓" : "")
<span>Show Toolbar</span>
</SmDropdownMenuCheckboxItem>
<SmDropdownMenuCheckboxItem class="su-dropdown-item" @bind-Checked="showSidebar">
@(context.Checked ? "✓" : "")
<span>Show Sidebar</span>
</SmDropdownMenuCheckboxItem>
</SmDropdownMenuContent>
</SmDropdownMenuPortal>
</SmDropdownMenuRoot>With Radio Group
Mutually exclusive options.
@code {
private string? selectedTheme = "system";
}
<SmDropdownMenuRoot>
<SmDropdownMenuTrigger class="su-dropdown-trigger">Theme</SmDropdownMenuTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuContent class="su-dropdown-content">
<SmDropdownMenuRadioGroup @bind-Value="selectedTheme" AriaLabel="Theme">
<SmDropdownMenuRadioItem class="su-dropdown-item" Value="light">
@(context.IsSelected ? "●" : "○") Light
</SmDropdownMenuRadioItem>
<SmDropdownMenuRadioItem class="su-dropdown-item" Value="dark">
@(context.IsSelected ? "●" : "○") Dark
</SmDropdownMenuRadioItem>
<SmDropdownMenuRadioItem class="su-dropdown-item" Value="system">
@(context.IsSelected ? "●" : "○") System
</SmDropdownMenuRadioItem>
</SmDropdownMenuRadioGroup>
</SmDropdownMenuContent>
</SmDropdownMenuPortal>
</SmDropdownMenuRoot>Grouped Items
Organize items into labeled groups.
<SmDropdownMenuRoot>
<SmDropdownMenuTrigger class="su-dropdown-trigger">Edit</SmDropdownMenuTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuContent class="su-dropdown-content">
<SmDropdownMenuGroup>
<SmDropdownMenuGroupLabel class="su-dropdown-label">Clipboard</SmDropdownMenuGroupLabel>
<SmDropdownMenuItem class="su-dropdown-item">Cut</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item">Copy</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item">Paste</SmDropdownMenuItem>
</SmDropdownMenuGroup>
<SmDropdownMenuSeparator class="su-dropdown-separator" />
<SmDropdownMenuGroup>
<SmDropdownMenuGroupLabel class="su-dropdown-label">Selection</SmDropdownMenuGroupLabel>
<SmDropdownMenuItem class="su-dropdown-item">Select All</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item">Deselect</SmDropdownMenuItem>
</SmDropdownMenuGroup>
</SmDropdownMenuContent>
</SmDropdownMenuPortal>
</SmDropdownMenuRoot>With Submenu
Nested menus that open on hover or keyboard navigation.
<SmDropdownMenuRoot>
<SmDropdownMenuTrigger class="su-dropdown-trigger">File</SmDropdownMenuTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuContent class="su-dropdown-content">
<SmDropdownMenuItem class="su-dropdown-item">New File</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item">Open File</SmDropdownMenuItem>
<SmDropdownMenuSeparator class="su-dropdown-separator" />
<SmDropdownMenuSub>
<SmDropdownMenuSubTrigger class="su-dropdown-item su-dropdown-subtrigger">
<span>Share</span>
<span class="chevron">›</span>
</SmDropdownMenuSubTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuSubContent class="su-dropdown-content">
<SmDropdownMenuItem class="su-dropdown-item">Email</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item">Messages</SmDropdownMenuItem>
<SmDropdownMenuSeparator class="su-dropdown-separator" />
<SmDropdownMenuItem class="su-dropdown-item">Copy Link</SmDropdownMenuItem>
</SmDropdownMenuSubContent>
</SmDropdownMenuPortal>
</SmDropdownMenuSub>
<SmDropdownMenuSub>
<SmDropdownMenuSubTrigger class="su-dropdown-item su-dropdown-subtrigger">
<span>Export As</span>
<span class="chevron">›</span>
</SmDropdownMenuSubTrigger>
<SmDropdownMenuPortal>
<SmDropdownMenuSubContent class="su-dropdown-content">
<SmDropdownMenuItem class="su-dropdown-item">PDF</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item">PNG</SmDropdownMenuItem>
<SmDropdownMenuItem class="su-dropdown-item">SVG</SmDropdownMenuItem>
</SmDropdownMenuSubContent>
</SmDropdownMenuPortal>
</SmDropdownMenuSub>
<SmDropdownMenuSeparator class="su-dropdown-separator" />
<SmDropdownMenuItem class="su-dropdown-item">Close</SmDropdownMenuItem>
</SmDropdownMenuContent>
</SmDropdownMenuPortal>
</SmDropdownMenuRoot>
@* CSS for subtrigger *@
<style>
.su-dropdown-subtrigger {
justify-content: space-between;
}
.su-dropdown-subtrigger .chevron {
margin-left: auto;
}
</style>Styling
Data Attributes
| Attribute | Values | Description |
|---|---|---|
| data-state | "open" | "closed" | Menu open state |
| data-highlighted | Present when focused | Item is keyboard-focused |
| data-disabled | Present when disabled | Item is disabled |
CSS Example
/* Trigger */
.su-dropdown-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: rgb(var(--su-background));
border: 1px solid rgb(var(--su-border));
border-radius: 4px;
cursor: pointer;
color: rgb(var(--su-foreground));
}
.su-dropdown-trigger[data-state="open"] {
background: rgb(var(--su-muted));
}
/* Content */
.su-dropdown-content {
min-width: 200px;
background: rgb(var(--su-card));
border: 1px solid rgb(var(--su-border));
border-radius: 8px;
box-shadow: 0 4px 12px rgb(var(--su-foreground) / 0.15);
padding: 4px;
}
/* Item */
.su-dropdown-item {
display: flex;
align-items: center;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
color: rgb(var(--su-foreground));
outline: none;
}
.su-dropdown-item[data-highlighted] {
background: rgb(var(--su-accent));
color: rgb(var(--su-accent-foreground));
}
.su-dropdown-item[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
/* Label */
.su-dropdown-label {
padding: 8px 12px;
font-size: 0.75rem;
font-weight: 600;
color: rgb(var(--su-muted-foreground));
}
/* Separator */
.su-dropdown-separator {
height: 1px;
background: rgb(var(--su-border));
margin: 4px 0;
}Accessibility
Keyboard Navigation
| Key | Action |
|---|---|
| Enter / Space | Open menu or select item |
| ArrowDown | Move focus to next item |
| ArrowUp | Move focus to previous item |
| ArrowRight | Open submenu (LTR) |
| ArrowLeft | Close submenu (LTR) |
| Home | Move focus to first item |
| End | Move focus to last item |
| Escape | Close menu or submenu |
ARIA Attributes
- Trigger:
Has
aria-haspopup="menu"andaria-expanded - Content:
Has
role="menu" - Items:
Have
role="menuitem", checkbox items haverole="menuitemcheckbox"