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 a web component in stellar-admin.js, so no additional JavaScript is required.
Control where the menu opens relative to its trigger with the side and align attributes on <sa-dropdown-menu-content>, which work like the ones on the Popover Tag Helper. By default the menu opens below the trigger (DropdownMenuSide.Bottom), lined up with its left edge (DropdownMenuAlign.Start).
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="funnel" 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"/>
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
Use <sa-dropdown-menu-item-link> for items that navigate to another page. It renders an <a role="menuitem"> and takes either a raw href or ASP.NET routing attributes (asp-page, asp-controller/asp-action, asp-route-*), which are resolved by the framework, exactly like <sa-button-link>. A plain <sa-dropdown-menu-item> renders a <button role="menuitem"> instead. Inside a form, give it type="submit" to submit the form.
<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-link asp-page="/Booking/Details" asp-route-id="TRP-4821">
<sa-icon name="luggage"/>
View Booking
</sa-dropdown-menu-item-link>
<!-- Link to an MVC controller action -->
<sa-dropdown-menu-item-link asp-controller="Search" asp-action="Index">
<sa-icon name="search"/>
Search Trips
</sa-dropdown-menu-item-link>
<sa-dropdown-menu-item-link asp-controller="User" asp-action="Profile">
<sa-icon name="circle-user-round"/>
My Profile
</sa-dropdown-menu-item-link>
</sa-dropdown-menu-group>
<sa-dropdown-menu-separator/>
<!-- External link via a raw href -->
<sa-dropdown-menu-item-link 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-link>
</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 menu 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="funnel" 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. To choose the id, set id here rather than on <sa-dropdown-menu-content>; an id on the content that differs from it throws, since the trigger could not find the menu.
<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 web component with the popover attribute, anchored to the trigger.
Prop
Type
<sa-dropdown-menu-item>
Renders a <button role="menuitem"> that performs an action. It defaults to type="button"; set type="submit" to submit an enclosing form. Use <sa-dropdown-menu-item-link> for navigation.
Prop
Type
<sa-dropdown-menu-item-link>
Renders an <a role="menuitem"> that links to another page. Supply the URL with a raw href or with routing attributes.
Prop
Type
Routing attributes
The link 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. Set id here rather than on <sa-dropdown-menu-sub-content>, for the same reason.
<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 popover anchored to its sub-trigger, accepting the same side and align values as <sa-dropdown-menu-content>.
Prop
Type