Command palette
tier 2 — classes
The Ctrl+K box: type, see matching commands, run one. A
<dialog>, so the top layer, the focus trap and Escape are the
platform's.
Open it
Live. This site registers one command per page with
sednaUi.palette.register, and writes the palette itself in its layout — the
markup below. Until something is registered, or with no palette in the page, the library
leaves the browser's own Ctrl+K alone. A command registered from C# carries an
Href and navigates. A run callback is a JavaScript function, and a
function does not cross into C# — navigation is what a serialisable command carries.
<button class="btn" type="button" onclick="sednaUi.palette.open()">
<i class="ri-command-line"></i> Open the palette
</button>
<span class="form-hint">…or press <span class="kbd">Ctrl</span> <span class="kbd">K</span> anywhere on this site.</span>
Registering commands
register() replaces the whole list, so call it again whenever what is
available changes — after a permission check, or on navigation. keywords makes
a command findable by a word that is not in its label.
// JavaScript — a command may run a callback.
sednaUi.palette.register([
{ label: 'Dispatch this order', icon: 'ri-send-plane-line', group: 'Orders',
note: 'Reserved lines only', keywords: 'send ship release',
run: () => dispatchCurrentOrder() },
{ label: 'Open the queue', icon: 'ri-inbox-line', group: 'Go to', href: '/queue' },
]);
// C# — a command registered from .NET carries an Href, never a callback: the
// function does not cross into C#, so navigation is the one action a
// serialisable command can perform.
await Ui.RegisterCommandsAsync(
[
new PaletteCommand { Label = "Open the queue", Icon = "ri-inbox-line",
Group = "Go to", Href = "/queue" },
new PaletteCommand { Label = "Account settings", Icon = "ri-user-line",
Group = "Go to", Href = "/settings", Keywords = "profile theme" },
]);
// register() REPLACES the list. Call it again whenever what is available
// changes — after a permission check, or on navigation.
//
// Groups are shown in registration order and dropped once a query has
// reordered the list, because a heading over unrelated results is worse than
// no heading. Ctrl/Cmd-K is wired for you, and does nothing until at least one
// command is registered, so the browser's own binding is left alone.
Matching, and nothing found
The two states side by side, laid out statically here so both are visible at once, with
.palette-footer carrying the keys as .kbd literals.
.palette-group heads a run of commands from the same group and is dropped once
a query has reordered the list; its <li> needs
role="presentation", because a listbox may own only
options and a bare <li> breaks
aria-required-children.
- Orders
- Dispatch this order Reserved lines only
- Discard the draft Cannot be undone
<div class="sedna-col sedna-gap-3" style="max-width:520px">
<div class="palette" style="position:static; inset:auto; display:flex; flex-direction:column">
<input class="palette-input" type="text" value="dis" readonly aria-label="Run a command" />
<ul class="palette-list" role="listbox" aria-label="Commands">
<li role="presentation"><div class="palette-group">Orders</div></li>
<li class="palette-item" role="option" aria-selected="true">
<i class="ri-send-plane-line"></i> Dispatch this order
<span class="palette-item-note">Reserved lines only</span>
</li>
<li class="palette-item" role="option" aria-selected="false">
<i class="ri-close-circle-line"></i> Discard the draft
<span class="palette-item-note">Cannot be undone</span>
</li>
</ul>
<div class="palette-footer">
<span><span class="kbd">↑</span> <span class="kbd">↓</span> to move</span>
<span><span class="kbd">Enter</span> to run</span>
<span><span class="kbd">Esc</span> to close</span>
</div>
</div>
<div class="palette" style="position:static; inset:auto; display:flex; flex-direction:column">
<input class="palette-input" type="text" value="refund" readonly aria-label="Run a command" />
<div class="palette-empty">No command matches “refund”.</div>
</div>
</div>
The markup
What the library builds, for an app rendering its own command list — if you are calling
register() you write none of it. The list is a real
role="listbox" of role="option" items with the input as the
combobox owning it through aria-activedescendant,
aria-selected is the keyboard's position and not :hover, and
.palette-empty should name what was searched rather than say “no
results”; the ranking is sednaUi._.score, shared with the
header search so the two cannot rank one query differently.
<!-- The palette an app writes, once, in its layout. sednaUi.palette fills the list
from the templates and draws nothing itself, so every word here is yours to
translate. Leave the list empty: the script owns its children. The group and
empty templates are optional. -->
<dialog class="palette" data-palette aria-label="Commands">
<input class="palette-input" type="text" placeholder="Search commands…" aria-label="Search commands" />
<ul class="palette-list" aria-label="Commands"></ul>
<div class="palette-footer">
<span><span class="kbd">↑</span> <span class="kbd">↓</span> to move</span>
<span><span class="kbd">Enter</span> to run</span>
<span><span class="kbd">Esc</span> to close</span>
</div>
<!-- One command. data-label and data-note take text; data-icon takes the command's
icon class. A slot with nothing to say is removed. role="presentation" on the
<li> is load-bearing: a listbox may only own options. -->
<template data-palette-item>
<li role="presentation">
<div class="palette-item" role="option">
<i data-icon aria-hidden="true"></i><span data-label></span>
<span class="palette-item-note" data-note></span>
</div>
</li>
</template>
<!-- A heading over a run of commands from one group, while nothing is typed. -->
<template data-palette-group>
<li role="presentation"><div class="palette-group" data-group></div></li>
</template>
<!-- In place of the options. Say what was searched, not just "no results". -->
<template data-palette-empty>
<li role="presentation"><div class="palette-empty">Nothing matches “<span data-query></span>”.</div></li>
</template>
</dialog>