Command
A searchable menu of commands or options, navigated with the keyboard from a single search input
<sa-command label="Search Voyager" class="h-auto w-full max-w-md border shadow-md">
<sa-command-input placeholder="Type a command or search..." />
<sa-command-list>
<sa-command-empty>No results found.</sa-command-empty>
<sa-command-group heading="Suggestions">
<sa-command-item>
<sa-icon name="plane" />
<span>Search flights</span>
</sa-command-item>
<sa-command-item keywords="accommodation stay">
<sa-icon name="bed-double" />
<span>Find a hotel</span>
</sa-command-item>
<sa-command-item disabled="true">
<sa-icon name="car" />
<span>Rent a car</span>
</sa-command-item>
</sa-command-group>
<sa-command-separator />
<sa-command-group heading="Account">
<sa-command-item>
<sa-icon name="luggage" />
<span>My trips</span>
<sa-command-shortcut>⌘T</sa-command-shortcut>
</sa-command-item>
<sa-command-item>
<sa-icon name="credit-card" />
<span>Payment methods</span>
<sa-command-shortcut>⌘B</sa-command-shortcut>
</sa-command-item>
<sa-command-item>
<sa-icon name="settings" />
<span>Settings</span>
<sa-command-shortcut>⌘S</sa-command-shortcut>
</sa-command-item>
</sa-command-group>
</sa-command-list>
</sa-command>Usage
A command menu is composed of <sa-command> wrapping a <sa-command-input> and a <sa-command-list>. The list holds <sa-command-item> entries, optionally organised into <sa-command-group> sections divided by <sa-command-separator>, and a <sa-command-empty> message shown when nothing matches. Add a trailing keyboard hint to an item with <sa-command-shortcut>.
<sa-command label="...">
<sa-command-input placeholder="..." />
<sa-command-list>
<sa-command-empty>No results found.</sa-command-empty>
<sa-command-group heading="...">
<sa-command-item>...</sa-command-item>
<sa-command-item>
...
<sa-command-shortcut>...</sa-command-shortcut>
</sa-command-item>
</sa-command-group>
<sa-command-separator />
<sa-command-group heading="...">
<sa-command-link-item href="...">...</sa-command-link-item>
</sa-command-group>
</sa-command-list>
</sa-command>As the user types, items are fuzzy-matched against their text and ranked within their group, so the best match in each group rises to the top while the groups themselves keep their order. Groups with no matches are hidden, and the empty message appears when no item matches at all. Set value on an item to match against something other than its text, and keywords to add extra search terms, such as synonyms.
The menu is driven by the sel-command web component, so filtering and keyboard navigation need no additional JavaScript.
Examples
Dialog
Wrap the command menu in <sa-command-dialog> to present it as a command palette. It is a Dialog, so open it the same ways: an invoker button with commandfor and command="show-modal", as below, or showModal() from script. The search input is focused when the dialog opens, and the search is cleared when it closes.
<div class="flex justify-center">
<sa-button variant="ButtonVariant.Outline" commandfor="--command-dialog" command="show-modal">
Open command palette
</sa-button>
</div>
<sa-command-dialog id="--command-dialog">
<sa-command>
<sa-command-input placeholder="Type a command or search..." />
<sa-command-list>
<sa-command-empty>No results found.</sa-command-empty>
<sa-command-group heading="Destinations">
<sa-command-item value="lisbon" keywords="portugal">
<sa-icon name="map-pin" />
<span>Lisbon</span>
</sa-command-item>
<sa-command-item value="kyoto" keywords="japan">
<sa-icon name="map-pin" />
<span>Kyoto</span>
</sa-command-item>
<sa-command-item value="cape-town" keywords="south africa">
<sa-icon name="map-pin" />
<span>Cape Town</span>
</sa-command-item>
</sa-command-group>
<sa-command-separator />
<sa-command-group heading="Bookings">
<sa-command-link-item href="#">
<sa-icon name="ticket" />
<span>Upcoming bookings</span>
</sa-command-link-item>
<sa-command-link-item href="#">
<sa-icon name="calendar" />
<span>Travel calendar</span>
</sa-command-link-item>
</sa-command-group>
</sa-command-list>
</sa-command>
</sa-command-dialog>The dialog carries a visually hidden title and description for assistive technology, defaulting to "Command Palette" and "Search for a command to run...". Override them with title and description when the palette has a more specific purpose.
Keyboard shortcut
Command palettes are usually opened with a keyboard shortcut. The Tag Helper does not bind one for you, so pick the shortcut that suits your application and toggle the dialog from a keydown listener. This example uses ⌘K on macOS and Ctrl+K elsewhere, and closes the palette once an item is chosen.
<p class="text-muted-foreground text-center text-sm">
Press
<sa-kbd-group>
<sa-kbd>⌘</sa-kbd>
<sa-kbd>K</sa-kbd>
</sa-kbd-group>
or
<sa-kbd-group>
<sa-kbd>Ctrl</sa-kbd>
<sa-kbd>K</sa-kbd>
</sa-kbd-group>
to search Voyager
</p>
<sa-command-dialog id="--command-shortcut-dialog">
<sa-command>
<sa-command-input placeholder="Type a command or search..." />
<sa-command-list>
<sa-command-empty>No results found.</sa-command-empty>
<sa-command-group heading="Suggestions">
<sa-command-item>
<sa-icon name="plane" />
<span>Search flights</span>
</sa-command-item>
<sa-command-item keywords="accommodation stay">
<sa-icon name="bed-double" />
<span>Find a hotel</span>
</sa-command-item>
<sa-command-item>
<sa-icon name="car" />
<span>Rent a car</span>
</sa-command-item>
</sa-command-group>
<sa-command-separator />
<sa-command-group heading="Account">
<sa-command-item>
<sa-icon name="luggage" />
<span>My trips</span>
<sa-command-shortcut>⌘T</sa-command-shortcut>
</sa-command-item>
<sa-command-item>
<sa-icon name="settings" />
<span>Settings</span>
<sa-command-shortcut>⌘S</sa-command-shortcut>
</sa-command-item>
</sa-command-group>
</sa-command-list>
</sa-command>
</sa-command-dialog>
<script>
(() => {
const dialog = document.getElementById('--command-shortcut-dialog');
// ⌘K on macOS, Ctrl+K elsewhere, toggles the palette. Escape closes it natively.
// Inside the palette Ctrl+K moves to the previous item, which marks the event as handled.
document.addEventListener('keydown', (event) => {
if (event.defaultPrevented) {
return;
}
if (event.key.toLowerCase() === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
dialog.open ? dialog.close() : dialog.showModal();
}
});
// Close the palette once an item is chosen.
dialog.addEventListener('itemselect', () => dialog.close());
})();
</script>Inside the menu, Ctrl+K moves to the previous item, and the command menu marks that key press as handled. Checking event.defaultPrevented keeps the shortcut from also closing the palette.
Checked items
Set checked="true" to show a check mark at the end of an item, for example to mark the current choice. Listen for the bubbling itemselect event to react when an item is chosen: event.detail.value holds the item's value and event.target is the item itself, so this example moves the check mark to the chosen currency.
<sa-command id="--currency-command" label="Display currency" class="h-auto w-full max-w-sm border shadow-md">
<sa-command-input placeholder="Search currencies..." />
<sa-command-list>
<sa-command-empty>No currency found.</sa-command-empty>
<sa-command-group heading="Display prices in">
<sa-command-item value="USD" keywords="us dollar united states">
<span>US dollar</span>
<span class="text-muted-foreground">USD</span>
</sa-command-item>
<sa-command-item value="EUR" keywords="euro europe" checked="true">
<span>Euro</span>
<span class="text-muted-foreground">EUR</span>
</sa-command-item>
<sa-command-item value="GBP" keywords="pound sterling united kingdom">
<span>British pound</span>
<span class="text-muted-foreground">GBP</span>
</sa-command-item>
<sa-command-item value="JPY" keywords="yen japan">
<span>Japanese yen</span>
<span class="text-muted-foreground">JPY</span>
</sa-command-item>
<sa-command-item value="ZAR" keywords="rand south africa">
<span>South African rand</span>
<span class="text-muted-foreground">ZAR</span>
</sa-command-item>
</sa-command-group>
</sa-command-list>
</sa-command>
<p class="text-muted-foreground text-sm" id="--currency-output">Prices are shown in EUR.</p>
<script>
(() => {
const command = document.getElementById('--currency-command');
const output = document.getElementById('--currency-output');
// "itemselect" bubbles from the chosen item; move the check mark to it.
command.addEventListener('itemselect', (event) => {
command.querySelectorAll('[data-checked]').forEach((item) => item.removeAttribute('data-checked'));
event.target.setAttribute('data-checked', 'true');
output.textContent = `Prices are shown in ${event.detail.value}.`;
});
})();
</script>The check mark is hidden on items that have a shortcut.
Scrollable
The list has a maximum height and scrolls when there are more items than fit. The highlighted item is kept in view as the user moves through the list, without scrolling the rest of the page.
<sa-command label="Search Voyager" class="h-auto w-full max-w-md border shadow-md">
<sa-command-input placeholder="Type a command or search..." />
<sa-command-list>
<sa-command-empty>No results found.</sa-command-empty>
<sa-command-group heading="Destinations">
<sa-command-item value="lisbon" keywords="portugal">
<sa-icon name="map-pin" />
<span>Lisbon</span>
</sa-command-item>
<sa-command-item value="kyoto" keywords="japan">
<sa-icon name="map-pin" />
<span>Kyoto</span>
</sa-command-item>
<sa-command-item value="cape-town" keywords="south africa">
<sa-icon name="map-pin" />
<span>Cape Town</span>
</sa-command-item>
<sa-command-item value="reykjavik" keywords="iceland">
<sa-icon name="map-pin" />
<span>Reykjavík</span>
</sa-command-item>
<sa-command-item value="buenos-aires" keywords="argentina">
<sa-icon name="map-pin" />
<span>Buenos Aires</span>
</sa-command-item>
<sa-command-item value="marrakesh" keywords="morocco">
<sa-icon name="map-pin" />
<span>Marrakesh</span>
</sa-command-item>
<sa-command-item value="vancouver" keywords="canada">
<sa-icon name="map-pin" />
<span>Vancouver</span>
</sa-command-item>
<sa-command-item value="queenstown" keywords="new zealand">
<sa-icon name="map-pin" />
<span>Queenstown</span>
</sa-command-item>
</sa-command-group>
<sa-command-separator />
<sa-command-group heading="Bookings">
<sa-command-item>
<sa-icon name="plane" />
<span>Search flights</span>
<sa-command-shortcut>⌘F</sa-command-shortcut>
</sa-command-item>
<sa-command-item keywords="accommodation stay">
<sa-icon name="bed-double" />
<span>Find a hotel</span>
<sa-command-shortcut>⌘H</sa-command-shortcut>
</sa-command-item>
<sa-command-item>
<sa-icon name="car" />
<span>Rent a car</span>
</sa-command-item>
<sa-command-item>
<sa-icon name="train-front" />
<span>Book a train</span>
</sa-command-item>
<sa-command-item keywords="tours activities">
<sa-icon name="ticket" />
<span>Experiences</span>
</sa-command-item>
</sa-command-group>
<sa-command-separator />
<sa-command-group heading="Trip tools">
<sa-command-item>
<sa-icon name="calendar" />
<span>Travel calendar</span>
<sa-command-shortcut>⌘C</sa-command-shortcut>
</sa-command-item>
<sa-command-item keywords="packing">
<sa-icon name="list-checks" />
<span>Packing list</span>
</sa-command-item>
<sa-command-item keywords="exchange rates">
<sa-icon name="coins" />
<span>Currency converter</span>
</sa-command-item>
<sa-command-item keywords="passport entry requirements">
<sa-icon name="stamp" />
<span>Visa requirements</span>
</sa-command-item>
<sa-command-item keywords="forecast">
<sa-icon name="cloud-sun" />
<span>Weather</span>
</sa-command-item>
<sa-command-item keywords="time zones">
<sa-icon name="globe" />
<span>World clock</span>
</sa-command-item>
</sa-command-group>
<sa-command-separator />
<sa-command-group heading="Account">
<sa-command-item>
<sa-icon name="luggage" />
<span>My trips</span>
<sa-command-shortcut>⌘T</sa-command-shortcut>
</sa-command-item>
<sa-command-item keywords="points miles">
<sa-icon name="award" />
<span>Loyalty rewards</span>
</sa-command-item>
<sa-command-item>
<sa-icon name="credit-card" />
<span>Payment methods</span>
<sa-command-shortcut>⌘B</sa-command-shortcut>
</sa-command-item>
<sa-command-item keywords="travellers companions">
<sa-icon name="users" />
<span>Saved travellers</span>
</sa-command-item>
<sa-command-item keywords="alerts">
<sa-icon name="bell" />
<span>Notifications</span>
</sa-command-item>
<sa-command-item>
<sa-icon name="settings" />
<span>Settings</span>
<sa-command-shortcut>⌘S</sa-command-shortcut>
</sa-command-item>
</sa-command-group>
</sa-command-list>
</sa-command>Links
Use <sa-command-link-item> to render an item as an <a> that navigates when chosen, with either the mouse or Enter. The Dialog example above uses link items for its Bookings group.
The URL can be specified a couple of ways:
- You can use the
hrefproperty to specify any valid URL as per thehrefproperty of the anchor element. - You can use any of the attributes that are valid for specifying the URL for the ASP.NET Core Anchor Tag Helper, such as
asp-controller,asp-action,asp-page, etc. Please refer to the Anchor Tag Helper documentation for more examples.
Server-side search
When the items come from a large or remote data set, set filter="CommandFilter.None" and search on the server instead. The command menu then never hides or ranks items itself. Attributes that <sa-command-input> does not recognise are rendered on the <input>, so a library such as htmx can send the search as the user types and replace the contents of the list with the results.
<sa-command id="trip-search" filter="CommandFilter.None" label="Search trips">
<sa-command-input name="q"
placeholder="Search destinations and bookings..."
hx-get="?handler=Search"
hx-trigger="input changed delay:200ms"
hx-target="#trip-search-list"
hx-sync="this:replace" />
<sa-command-list>
<partial name="_TripResults" model="Model.Results" />
</sa-command-list>
</sa-command>The list's id is the command's id followed by -list. The handler renders the results as a partial view:
@model TripResults
<sa-command-empty>No trips match "@Model.Query".</sa-command-empty>
@if (Model.Destinations.Count > 0)
{
<sa-command-group heading="Destinations">
@foreach (var destination in Model.Destinations)
{
<sa-command-item value="@destination.Slug">@destination.City</sa-command-item>
}
</sa-command-group>
}The command menu notices when its items change. It prepares the new items, shows the empty message when there are none, and highlights the first result, so the keyboard keeps working without any extra script. Because the swap replaces everything inside the list, the partial must include <sa-command-empty> itself.
Accessibility
The command menu renders the combobox pattern for you. The search input is a combobox that controls the listbox list, and every item is an option. Focus stays in the input while the Up and Down arrows move the highlighted item, which is announced through aria-activedescendant. Home and End jump to the first and last item, Ctrl+N or Ctrl+J move down and Ctrl+P or Ctrl+K move up, and Enter chooses the highlighted item. Disabled items render aria-disabled="true" and are skipped. Set loop="true" on <sa-command> to wrap from the last item back to the first.
Give the menu an accessible name with label, which renders a visually hidden label for the search input. A placeholder is not a label.
API Reference
<sa-command>
Renders the <sel-command> web component that filters the items and handles keyboard navigation, forwarding any global attributes.
Prop
Type
When an item is chosen, with a click or Enter, the item dispatches a bubbling itemselect event whose detail.value is the item's value.
<sa-command-dialog>
Renders a modal <dialog> that presents a command menu as a command palette. Place a <sa-command> inside it.
Prop
Type
<sa-command-input>
Renders the search <input> with a search icon. Attributes the tag does not recognise, such as name, placeholder, or request attributes for server-side search, are rendered on the <input>.
<sa-command-list>
Renders a <div role="listbox"> holding the items. This is the element to target when replacing the items with server-rendered results.
<sa-command-empty>
Renders a <div> shown only when no item is present or matches the search.
<sa-command-group>
Renders a <div> grouping related items under an optional heading. The group is hidden when none of its items match the search.
Prop
Type
<sa-command-item>
Renders a <div role="option">, forwarding any global attributes.
Prop
Type
<sa-command-link-item>
Renders an <a role="option"> that navigates to its target when chosen. It accepts the same checked, disabled, keywords and value attributes as <sa-command-item>.
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-command-separator>
Renders a <div role="separator"> between groups. Separators are hidden while a search is active.
<sa-command-shortcut>
Renders a <span> holding a keyboard hint at the end of an item. Its text is not matched by the search.