Dropdown Menu
Displays a menu of actions or options triggered by a button
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="circle-user-round" class="text-muted-foreground"/>
Ibn Battuta
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-label>My Account</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-group>
<sa-dropdown-menu-item>
<sa-icon name="user"/>
Profile
<sa-dropdown-menu-shortcut>⇧⌘P</sa-dropdown-menu-shortcut>
</sa-dropdown-menu-item>
<sa-dropdown-menu-item>
<sa-icon name="luggage"/>
My Bookings
<sa-dropdown-menu-shortcut>⌘B</sa-dropdown-menu-shortcut>
</sa-dropdown-menu-item>
<sa-dropdown-menu-item>
<sa-icon name="settings"/>
Settings
<sa-dropdown-menu-shortcut>⌘,</sa-dropdown-menu-shortcut>
</sa-dropdown-menu-item>
</sa-dropdown-menu-group>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-item variant="DropdownMenuItemVariant.Destructive">
<sa-icon name="log-out"/>
Log out
<sa-dropdown-menu-shortcut>⇧⌘Q</sa-dropdown-menu-shortcut>
</sa-dropdown-menu-item>
</sa-dropdown-menu-content>
</sa-dropdown-menu>Usage
<sa-dropdown-menu> groups a trigger and its menu. The <sa-dropdown-menu-trigger> renders a <sa-button> (defaulting to ButtonVariant.Outline) that opens the menu using the HTML Popover API, and <sa-dropdown-menu-content> holds the menu items.
Compose the content from <sa-dropdown-menu-item> entries, grouping and labelling them with <sa-dropdown-menu-label>, <sa-dropdown-menu-separator> and <sa-dropdown-menu-group>. Add a trailing keyboard hint with <sa-dropdown-menu-shortcut>, and mark an item as dangerous with variant="DropdownMenuItemVariant.Destructive".
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">Open Menu</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-label>My Account</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-item>
<sa-icon name="user"/>
Profile
<sa-dropdown-menu-shortcut>⇧⌘P</sa-dropdown-menu-shortcut>
</sa-dropdown-menu-item>
<sa-dropdown-menu-item variant="DropdownMenuItemVariant.Destructive">
<sa-icon name="log-out"/>
Log out
</sa-dropdown-menu-item>
</sa-dropdown-menu-content>
</sa-dropdown-menu>Keyboard navigation, type-ahead, roving focus and close-on-select are handled for you — the menu is driven by the sel-dropdown-menu web component, so no additional JavaScript is required.
Control where the menu opens relative to its trigger with the position attribute on <sa-dropdown-menu-content>. It accepts the same PositionArea values as the Popover Tag Helper and defaults to opening below the trigger.
Examples
Checkbox items
Use <sa-dropdown-menu-checkbox-item> for options that can be toggled independently. Set checked="true" to render an item as selected. The menu stays open as items are toggled.
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="sliders-horizontal" class="text-muted-foreground"/>
Trip Filters
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-label>Show Categories</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-checkbox-item checked="true">Flights</sa-dropdown-menu-checkbox-item>
<sa-dropdown-menu-checkbox-item checked="true">Accommodation</sa-dropdown-menu-checkbox-item>
<sa-dropdown-menu-checkbox-item>Car Rental</sa-dropdown-menu-checkbox-item>
</sa-dropdown-menu-content>
</sa-dropdown-menu>Radio group
Wrap <sa-dropdown-menu-radio-item> elements in a <sa-dropdown-menu-radio-group> for a set of mutually exclusive options. The group's value attribute determines which item is selected.
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="filter" class="text-muted-foreground"/>
Booking Status
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-label>Filter by Status</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-radio-group value="confirmed">
<sa-dropdown-menu-radio-item value="confirmed">Confirmed</sa-dropdown-menu-radio-item>
<sa-dropdown-menu-radio-item value="pending">Pending</sa-dropdown-menu-radio-item>
<sa-dropdown-menu-radio-item value="cancelled">Cancelled</sa-dropdown-menu-radio-item>
</sa-dropdown-menu-radio-group>
</sa-dropdown-menu-content>
</sa-dropdown-menu>Submenus
Nest a <sa-dropdown-menu-sub> containing a <sa-dropdown-menu-sub-trigger> and <sa-dropdown-menu-sub-content> to create a submenu. Submenus open on hover or with the → key, and close with ←.
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="ellipsis" class="text-muted-foreground"/>
Trip Actions
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-item>
<sa-icon name="ticket"/>
View Tickets
</sa-dropdown-menu-item>
<sa-dropdown-menu-item>
<sa-icon name="calendar-plus"/>
Add to Calendar
</sa-dropdown-menu-item>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-sub>
<sa-dropdown-menu-sub-trigger>
<sa-icon name="map-pin"/>
Add Destination
</sa-dropdown-menu-sub-trigger>
<sa-dropdown-menu-sub-content class="w-44">
<sa-dropdown-menu-item>Paris</sa-dropdown-menu-item>
<sa-dropdown-menu-item>Bangkok</sa-dropdown-menu-item>
<sa-dropdown-menu-item>Kyoto</sa-dropdown-menu-item>
<sa-dropdown-menu-item>Cape Town</sa-dropdown-menu-item>
</sa-dropdown-menu-sub-content>
</sa-dropdown-menu-sub>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-item variant="DropdownMenuItemVariant.Destructive">
<sa-icon name="trash-2"/>
Cancel Trip
</sa-dropdown-menu-item>
</sa-dropdown-menu-content>
</sa-dropdown-menu>Inset
Set inset="true" on an item, label or sub-trigger to indent it by the width of the leading icon/checkbox column. Use it so text-only items and labels line up with sibling items that have a leading icon or a checkbox/radio indicator.
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="eye" class="text-muted-foreground"/>
Trip View
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<!-- These items have a leading icon, so their label starts past the icon column -->
<sa-dropdown-menu-group>
<sa-dropdown-menu-item>
<sa-icon name="map"/>
Map
</sa-dropdown-menu-item>
<sa-dropdown-menu-item>
<sa-icon name="list"/>
List
</sa-dropdown-menu-item>
<sa-dropdown-menu-item>
<sa-icon name="calendar"/>
Calendar
</sa-dropdown-menu-item>
</sa-dropdown-menu-group>
<sa-dropdown-menu-separator/>
<!-- inset="true" indents the label and items so their text lines up with the icon'd items above -->
<sa-dropdown-menu-label inset="true">Sort by</sa-dropdown-menu-label>
<sa-dropdown-menu-group>
<sa-dropdown-menu-item inset="true">Price</sa-dropdown-menu-item>
<sa-dropdown-menu-item inset="true">Rating</sa-dropdown-menu-item>
<sa-dropdown-menu-item inset="true">Duration</sa-dropdown-menu-item>
</sa-dropdown-menu-group>
</sa-dropdown-menu-content>
</sa-dropdown-menu>Links
An item becomes a navigation link when you give it a URL — either a raw href or ASP.NET routing attributes (asp-page, asp-controller/asp-action, asp-route-*). When any of these is present the item renders as an <a role="menuitem">. Otherwise it stays a <div role="menuitem">. Routing attributes are resolved by the framework, exactly like <sa-linkbutton>.
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="compass" class="text-muted-foreground"/>
Quick Links
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-label>Navigate</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-group>
<!-- Link to a Razor Page with a route value -->
<sa-dropdown-menu-item asp-page="/Booking/Details" asp-route-id="TRP-4821">
<sa-icon name="luggage"/>
View Booking
</sa-dropdown-menu-item>
<!-- Link to an MVC controller action -->
<sa-dropdown-menu-item asp-controller="Search" asp-action="Index">
<sa-icon name="search"/>
Search Trips
</sa-dropdown-menu-item>
<sa-dropdown-menu-item asp-controller="User" asp-action="Profile">
<sa-icon name="circle-user-round"/>
My Profile
</sa-dropdown-menu-item>
</sa-dropdown-menu-group>
<sa-dropdown-menu-separator/>
<!-- External link via a raw href -->
<sa-dropdown-menu-item href="https://help.voyager.travel" target="_blank" rel="noopener noreferrer">
<sa-icon name="life-buoy"/>
Help Center
<sa-icon name="external-link" class="ml-auto size-4"/>
</sa-dropdown-menu-item>
</sa-dropdown-menu-content>
</sa-dropdown-menu>Handling clicks
For non-link items, attach a click handler however you like — an inline onclick or addEventListener("click", ...). Because the sel-dropdown-menu web component dispatches a real click on the focused item for both mouse and keyboard activation (Enter / Space), your handler runs for either, and the menu closes on select afterwards.
<sa-stack gap="StackGap.Small" align="StackAlign.Center">
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="ellipsis" class="text-muted-foreground"/>
Trip Actions
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-label>Trip #TRV-987</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<!-- A non-link item receives click events on any element: here via an inline onclick -->
<sa-dropdown-menu-item onclick="voyagerTripAction('Trip link copied to clipboard')">
<sa-icon name="link"/>
Copy Trip Link
</sa-dropdown-menu-item>
<!-- ...or wired up in script with addEventListener (see below) -->
<sa-dropdown-menu-item id="--duplicate-trip-item">
<sa-icon name="copy"/>
Duplicate Trip
</sa-dropdown-menu-item>
<sa-dropdown-menu-item id="--print-itinerary-item">
<sa-icon name="printer"/>
Print Itinerary
</sa-dropdown-menu-item>
</sa-dropdown-menu-content>
</sa-dropdown-menu>
<p class="text-muted-foreground text-sm" id="--trip-action-output">
Choose an action from the menu.
</p>
</sa-stack>
<script>
// The sel-dropdown-menu web component dispatches a real click on the focused item for
// both mouse clicks and keyboard activation (Enter / Space), so a plain click listener
// works for either. The menu closes on select after your handler runs.
function voyagerTripAction(message) {
document.getElementById('--trip-action-output').textContent = message;
}
document
.getElementById('--duplicate-trip-item')
.addEventListener('click', () => voyagerTripAction('Trip duplicated'));
document
.getElementById('--print-itinerary-item')
.addEventListener('click', () => voyagerTripAction('Sending itinerary to the printer...'));
</script>Close on click
By default a plain <sa-dropdown-menu-item> closes the menu when activated, while checkbox and radio items stay open so you can adjust several at once. Override that per item with close-on-click: set close-on-click="false" on a plain item to keep the menu open (handy for a repeatable action), or close-on-click="true" on a checkbox/radio item to apply the choice and dismiss the menu. It applies to both mouse and keyboard activation.
<sa-stack gap="StackGap.Small" align="StackAlign.Center">
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="settings-2" class="text-muted-foreground"/>
Booking Options
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-64">
<sa-dropdown-menu-label>Availability</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<!-- A plain item closes the menu by default; close-on-click="false" keeps it
open so you can click it repeatedly. -->
<sa-dropdown-menu-item close-on-click="false" onclick="voyagerRefresh()">
<sa-icon name="refresh-cw"/>
Refresh availability
</sa-dropdown-menu-item>
<sa-dropdown-menu-separator/>
<sa-dropdown-menu-label>Preferences</sa-dropdown-menu-label>
<!-- Checkbox items stay open by default... -->
<sa-dropdown-menu-checkbox-item checked="true">Include breakfast</sa-dropdown-menu-checkbox-item>
<!-- ...while close-on-click="true" applies the choice and dismisses the menu. -->
<sa-dropdown-menu-checkbox-item close-on-click="true">Flexible dates (apply & close)</sa-dropdown-menu-checkbox-item>
</sa-dropdown-menu-content>
</sa-dropdown-menu>
<p class="text-muted-foreground text-sm" id="--booking-options-output">Refreshed 0 times.</p>
</sa-stack>
<script>
(() => {
let count = 0;
window.voyagerRefresh = () => {
count++;
document.getElementById('--booking-options-output').textContent =
'Refreshed ' + count + ' time' + (count === 1 ? '' : 's') + ' — the menu stayed open.';
};
})();
</script>Reacting to checkbox changes
Checkbox items report toggles through a bubbling checkedchange event, so you can listen on the item, its enclosing <sa-dropdown-menu-group>, or any ancestor — a single listener on the group is usually cleanest. event.detail.checked is the item's new state. Checkbox items carry no value, so identify which one changed via event.target. The event fires for both mouse and keyboard activation, and the menu deliberately stays open so multiple toggles feel natural.
<sa-stack gap="StackGap.Small" align="StackAlign.Center">
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="sliders-horizontal" class="text-muted-foreground"/>
Trip Filters
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-label>Show Categories</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<!-- One "checkedchange" listener on the group catches every item (the event bubbles) -->
<sa-dropdown-menu-group id="--trip-filters">
<sa-dropdown-menu-checkbox-item checked="true">Flights</sa-dropdown-menu-checkbox-item>
<sa-dropdown-menu-checkbox-item checked="true">Accommodation</sa-dropdown-menu-checkbox-item>
<sa-dropdown-menu-checkbox-item>Car Rental</sa-dropdown-menu-checkbox-item>
</sa-dropdown-menu-group>
</sa-dropdown-menu-content>
</sa-dropdown-menu>
<p class="text-muted-foreground text-sm" id="--trip-filters-output"></p>
</sa-stack>
<script>
(() => {
const group = document.getElementById('--trip-filters');
const output = document.getElementById('--trip-filters-output');
// Checkbox items carry no value, so identify the toggled item via event.target.
// Seed the active set from whatever is checked on load.
const active = new Set(
[...group.querySelectorAll('[aria-checked="true"]')].map((el) => el.textContent.trim()),
);
const render = () => {
output.textContent = active.size ? 'Showing: ' + [...active].join(', ') : 'No categories selected';
};
// The menu stays open while toggling; the event fires for mouse and keyboard alike.
group.addEventListener('checkedchange', (event) => {
const label = event.target.textContent.trim();
event.detail.checked ? active.add(label) : active.delete(label);
render();
});
render();
})();
</script>Reacting to radio changes
Radio items report selection through a bubbling valuechange event. Listen on the <sa-dropdown-menu-radio-group> and read event.detail.value — the newly selected value — from a single handler (event.target is the selected item, if you need the element). The component enforces single-selection before the event fires, and the menu stays open.
<sa-stack gap="StackGap.Small" align="StackAlign.Center">
<sa-dropdown-menu>
<sa-dropdown-menu-trigger variant="ButtonVariant.Outline">
<sa-icon name="filter" class="text-muted-foreground"/>
Booking Status
</sa-dropdown-menu-trigger>
<sa-dropdown-menu-content class="w-56">
<sa-dropdown-menu-label>Filter by Status</sa-dropdown-menu-label>
<sa-dropdown-menu-separator/>
<!-- Listen once on the group; the event bubbles up from the selected radio item -->
<sa-dropdown-menu-radio-group id="--booking-status" value="confirmed">
<sa-dropdown-menu-radio-item value="confirmed">Confirmed</sa-dropdown-menu-radio-item>
<sa-dropdown-menu-radio-item value="pending">Pending</sa-dropdown-menu-radio-item>
<sa-dropdown-menu-radio-item value="cancelled">Cancelled</sa-dropdown-menu-radio-item>
</sa-dropdown-menu-radio-group>
</sa-dropdown-menu-content>
</sa-dropdown-menu>
<p class="text-muted-foreground text-sm" id="--booking-status-output"></p>
</sa-stack>
<script>
(() => {
const labels = { confirmed: 'Confirmed', pending: 'Pending', cancelled: 'Cancelled' };
const output = document.getElementById('--booking-status-output');
const show = (value) => {
output.textContent = 'Showing: ' + (labels[value] ?? value) + ' bookings';
};
// A single listener on the group gets the selected value from event.detail.value
// (event.target is the chosen item, if you need the element too). The menu stays open.
document.getElementById('--booking-status').addEventListener('valuechange', (event) => {
show(event.detail.value);
});
show('confirmed'); // reflect the group's initial value
})();
</script>Accessibility
The dropdown menu renders the menu pattern for you. The trigger gets aria-haspopup="menu", the content is role="menu" and every item is a menuitem, menuitemcheckbox or menuitemradio with aria-checked kept in sync. Groups, radio groups and separators carry their roles, and a disabled item renders aria-disabled="true" and is skipped by keyboard navigation.
When the menu opens, focus moves to the first enabled item. Up and Down arrows move between items and wrap around, Home and End jump to the first and last item, Right arrow opens a submenu, Left arrow and Escape close the current submenu and return focus to its trigger, Enter and Space activate the focused item, and typing a letter jumps to the next item starting with it.
Give the trigger visible text or an aria-label when it only contains an icon.
API Reference
<sa-dropdown-menu>
Groups a trigger and its menu content. It renders no element of its own — it only generates the shared id (honouring a user-supplied id) that connects the trigger to the menu.
The menu's color, appearance and item highlight are configured application-wide with ConfigureMenu. See Theming & Dark Mode.
<sa-dropdown-menu-trigger>
Renders a <button> styled as a <sa-button> that opens the menu via the native popovertarget attribute.
Prop
Type
<sa-dropdown-menu-content>
Renders the menu itself as a <sel-dropdown-menu> web component with the popover attribute, anchored to the trigger.
Prop
Type
<sa-dropdown-menu-item>
Renders a <div role="menuitem">, or an <a role="menuitem"> when an href or any routing attribute is supplied.
Prop
Type
Routing attributes
The item also accepts the same routing attributes as the ASP.NET Core Anchor Tag Helper for generating the href. Supply either an href or these attributes — not both.
Prop
Type
<sa-dropdown-menu-checkbox-item>
Renders a <div role="menuitemcheckbox"> with a leading check indicator. Toggling it raises a bubbling checkedchange event.
Prop
Type
<sa-dropdown-menu-radio-group>
Renders a <div role="group"> that enforces single selection among its radio items and raises a bubbling valuechange event when the selection changes.
Prop
Type
<sa-dropdown-menu-radio-item>
Renders a <div role="menuitemradio"> with a leading check indicator, checked when its value matches the group's value.
Prop
Type
<sa-dropdown-menu-label>
Renders a <div> used as a heading for a group of items.
Prop
Type
<sa-dropdown-menu-separator>
Renders a <div role="separator"> between groups of items.
<sa-dropdown-menu-shortcut>
Renders a <span> for a trailing keyboard hint.
<sa-dropdown-menu-group>
Renders a <div role="group"> for grouping related items.
<sa-dropdown-menu-sub>
Groups a sub-trigger and its submenu. Like <sa-dropdown-menu>, it renders no element of its own and only generates the shared id connecting the two.
<sa-dropdown-menu-sub-trigger>
Renders a <div role="menuitem"> with a trailing chevron that opens its submenu on hover or with the → key.
Prop
Type
<sa-dropdown-menu-sub-content>
Renders a submenu as a <sel-dropdown-menu> popover anchored to its sub-trigger, accepting the same position values as <sa-dropdown-menu-content>.
Prop
Type