Dropdown-menu
Component

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

bash
dotnet add package SummitUI

Anatomy

Import the components and structure them as follows:

razor
<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)
razor
@* 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

razor
<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.

razor
@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.

razor
@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.

razor
<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.

razor
<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

css
/* 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" and aria-expanded
  • Content: Has role="menu"
  • Items: Have role="menuitem", checkbox items have role="menuitemcheckbox"
An unhandled error has occurred. Reload X