CommandPalette
Use CCommandPalette for a finite collection of application commands that people can search and run without leaving their current task. It combines a native modal Dialog, one editable combobox, and grouped listbox options. The application still owns command registration, authorization, routing, and side effects.
Open and run a command
Pass immutable records through entries, give the Dialog a visible label, and handle values with onAction. The activator slot receives activator_attrs and activator_disabled. Spread the complete attribute map on one ordinary native activator. For CButton, also pass c-disabled="activator_disabled" because Button owns its disabled state.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class BasicCommandPalette(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteCommand(
value="open-settings",
label="Open settings",
keywords=("preferences",),
shortcut="Ctrl ,",
),
CCommandPaletteCommand(value="create-project", label="Create project"),
CCommandPaletteCommand(value="invite-teammate", label="Invite teammate"),
)
}
template = """
<section
class="command-palette-basic"
x-data="{lastAction:'none',lastQuery:'',lastOpen:'closed'}"
>
<h2>Workspace commands</h2>
<p>Search a small set of actions without leaving the current task.</p>
<c-CCommandPalette
label="Workspace commands"
c-entries="commands"
$c-props="{
onAction:(value)=>lastAction=value,
onQueryChange:(value)=>lastQuery=value,
onOpenChange:(value)=>lastOpen=value ? 'open' : 'closed',
}"
>
<c-fill name="activator" data="{ activator_attrs, activator_disabled }">
<c-CButton
variant="solid"
c-disabled="activator_disabled"
c-attrs="activator_attrs"
>
Open command palette
</c-CButton>
</c-fill>
</c-CCommandPalette>
<output aria-live="polite">
State: <span x-text="lastOpen">closed</span>;
query: <span x-text="lastQuery || 'empty'">empty</span>;
action: <span x-text="lastAction">none</span>
</output>
</section>
"""
css = """
:where(.command-palette-basic) {
display: grid;
gap: 0.75rem;
justify-items: start;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-basic h2, .command-palette-basic p) { margin: 0; }
"""
preview = BasicCommandPalette()
preview # noqa: B018
Commands are callback-only options. They are not links, selected form values, or Menu items. Use a native navigation list or Menu when people need link semantics, modifier keys, a browser context menu, or copyable destinations.
Build records in Python
CCommandPaletteCommand, CCommandPaletteGroup, and CCommandPaletteSeparator are frozen value records. They do not render alone. Command values stay globally unique across top-level entries and groups. Separators are visual boundaries between top-level regions.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import (
CCommandPalette,
CCommandPaletteCommand,
CCommandPaletteGroup,
CCommandPaletteSeparator,
)
citry.register_library(citry_ui)
class PythonCommandRecords(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
entries = (
CCommandPaletteGroup(
label="Project navigation",
commands=(
CCommandPaletteCommand(value="project-overview", label="Open project overview"),
CCommandPaletteCommand(value="project-files", label="Browse project files"),
),
),
CCommandPaletteSeparator(),
CCommandPaletteGroup(
label="Draft actions",
commands=(
CCommandPaletteCommand(value="save-draft", label="Save draft", shortcut="Ctrl S"),
CCommandPaletteCommand(
value="delete-draft",
label="Delete draft",
description="Moves this draft to Trash",
intent="danger",
),
),
),
)
return {
"python_palette": CCommandPalette(
label="Project commands",
entries=entries,
open=True,
)
}
template = """
<section class="command-palette-records">
<h2>Frozen Python records</h2>
<p>The rendered palette preserves group, separator, and command order.</p>
{{ python_palette }}
</section>
"""
css = """
:where(.command-palette-records) {
display: grid;
gap: 0.75rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-records h2, .command-palette-records p) { margin: 0; }
"""
preview = PythonCommandRecords()
preview # noqa: B018
Search labels and aliases
Filtering normalizes labels, keywords, and the exact query with NFKC, collapses Unicode whitespace, trims, and applies locale-neutral lowercase. A command matches when the whole normalized query appears in its label or one keyword. Descriptions, shortcut hints, values, and slot content are not searched. Matches keep their server order and are never fuzzy-ranked.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class SearchAndEmpty(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteCommand(
value="theme",
label="Choose theme",
keywords=("appearance", "color mode"),
),
CCommandPaletteCommand(
value="light-mode",
label="Use light appearance",
keywords=("theme", "color mode"),
),
CCommandPaletteCommand(
value="dark-mode",
label="Use dark appearance",
keywords=("theme", "color mode"),
),
CCommandPaletteCommand(
value="managed-theme",
label="Use managed appearance",
keywords=("theme",),
disabled=True,
),
)
}
template = """
<section
class="command-palette-search"
x-data="{open:true,query:'theme'}"
>
<h2>Exact substring search</h2>
<div role="group" aria-label="Search examples">
<button type="button" @click="query='appearance'">Search appearance</button>
<button type="button" @click="query='zz'">Show no match</button>
<button type="button" @click="query='managed'">Show a disabled match</button>
<button type="button" @click="query=''">Clear search</button>
</div>
<c-CCommandPalette
label="Appearance commands"
c-entries="commands"
empty_label="No appearance commands match"
$c-props="{
open,
query,
onOpenChange:(value)=>open=value,
onQueryChange:(value)=>query=value,
}"
/>
<output>Owner query: <span x-text="query || 'empty'">theme</span></output>
</section>
"""
css = """
:where(.command-palette-search) {
display: grid;
gap: 0.75rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-search h2) { margin: 0; }
:where(.command-palette-search [role="group"]) {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
"""
preview = SearchAndEmpty()
preview # noqa: B018
Show disabled commands and shortcut hints
Disabled commands remain visible and searchable, expose disabled option state, and are skipped by active navigation. shortcut is presentational text only. The component never registers that key combination.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class DisabledAndShortcuts(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteCommand(
value="deploy-production",
label="Deploy production",
description="Unavailable until checks pass",
shortcut="Ctrl D",
disabled=True,
),
CCommandPaletteCommand(value="view-logs", label="View logs", shortcut="Ctrl L"),
CCommandPaletteCommand(
value="delete-environment",
label="Delete environment",
shortcut="Shift Delete",
intent="danger",
),
)
}
template = """
<section
class="command-palette-disabled"
dir="rtl"
x-data="{open:true,disabled:false,loop:true,last:'none'}"
>
<h2>Deployment commands</h2>
<div role="group" aria-label="Palette settings">
<label><input type="checkbox" x-model="disabled" /> Disable palette</label>
<label><input type="checkbox" x-model="loop" /> Loop navigation</label>
</div>
<c-CCommandPalette
label="Deployment commands"
c-entries="commands"
size="lg"
$c-props="{
open,
disabled,
loop,
onOpenChange:(value)=>open=value,
onAction:(value)=>last=value,
}"
/>
<output aria-live="polite">Action: <span x-text="last">none</span></output>
</section>
"""
css = """
:where(.command-palette-disabled) {
display: grid;
gap: 0.75rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-disabled h2) { margin: 0; }
:where(.command-palette-disabled [role="group"]) {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
@media (forced-colors: active) {
:where(.command-palette-disabled output) { border: 1px solid CanvasText; }
}
"""
preview = DisabledAndShortcuts()
preview # noqa: B018
Use intent="danger" to give a destructive command visual emphasis. It does not authorize the action or bypass disabled state.
Add safe visual adornments
The item_start and item_end slots receive immutable CCommandPaletteItemSlotData. Their output is decorative, inert, and hidden from the accessibility tree. Keep the owned label and description as the command's complete semantic content. Interactive controls, links, meaningful images, form controls, and custom elements are rejected.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class CommandAdornments(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteCommand(
value="create-release",
label="Create release",
description="Prepare notes and artifacts",
keywords=("publish",),
),
CCommandPaletteCommand(
value="open-preview",
label="Open preview",
description="Inspect the latest deployment",
keywords=("beta",),
),
)
}
template = """
<section class="command-palette-adornments" x-data="{open:true,last:'none'}">
<h2>Release commands with decoration</h2>
<c-CCommandPalette
label="Release commands"
c-entries="commands"
$c-props="{
open,
onOpenChange:(value)=>open=value,
onAction:(value)=>last=value,
}"
>
<c-fill
name="item_start"
data="{ value, label, description, keywords, shortcut, disabled, close_on_action, intent }"
>
<span class="command-palette-adornments__icon">◆</span>
</c-fill>
<c-fill
name="item_end"
data="{ value, label, description, keywords, shortcut, disabled, close_on_action, intent }"
>
<span class="command-palette-adornments__badge">Beta</span>
</c-fill>
</c-CCommandPalette>
<output aria-live="polite">Action: <span x-text="last">none</span></output>
</section>
"""
css = """
:where(.command-palette-adornments) {
display: grid;
gap: 0.75rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-adornments h2) { margin: 0; }
:where(.command-palette-adornments__badge) {
padding: 0.125rem 0.375rem;
border: 1px solid currentColor;
border-radius: 999px;
font-size: 0.6875rem;
}
:where(.command-palette-adornments__icon) { color: light-dark(#175cd3, #84adff); }
"""
preview = CommandAdornments()
preview # noqa: B018
Control open state and query text
Client open and query values own independent axes while supplied. User edits and dismissals are requests through onQueryChange and onOpenChange. If the owner retains its old value, the input, results, active command, focus, and Dialog remain on that accepted state.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class ControlledCommandPalette(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteCommand(value="workspace-alpha", label="Switch to Alpha workspace"),
CCommandPaletteCommand(value="workspace-bravo", label="Switch to Bravo workspace"),
CCommandPaletteCommand(value="workspace-charlie", label="Switch to Charlie workspace"),
)
}
template = """
<section
class="command-palette-controlled"
x-data="{
open:true,
query:'work',
controlOpen:true,
controlQuery:true,
acceptClose:false,
acceptQuery:false,
requests:[],
}"
>
<h2>Switch workspace</h2>
<div role="group" aria-label="Controlled palette settings">
<label><input type="checkbox" x-model="acceptClose" /> Accept close</label>
<label><input type="checkbox" x-model="acceptQuery" /> Accept query edits</label>
<button type="button" @click="controlOpen=false">Release open control</button>
<button type="button" @click="controlQuery=false">Release query control</button>
<button type="button" @click="controlOpen=true;open=true">Open from owner</button>
</div>
<c-CCommandPalette
label="Switch workspace"
c-entries="commands"
$c-props="{
open:controlOpen ? open : null,
query:controlQuery ? query : null,
onOpenChange:(value,detail)=>{
requests.push(`open:${value}:${detail.reason}`);
if (!controlOpen || value || acceptClose) open=value;
},
onQueryChange:(value,detail)=>{
requests.push(`query:${value}:${detail.reason}`);
if (!controlQuery || acceptQuery || detail.reason==='close') query=value;
},
}"
/>
<output aria-live="polite">
Owner: <span x-text="open ? 'open' : 'closed'">open</span>;
query: <span x-text="query || 'empty'">work</span>;
requests: <span x-text="requests.slice(-3).join(' | ') || 'none'">none</span>
</output>
</section>
"""
css = """
:where(.command-palette-controlled) {
display: grid;
gap: 0.75rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-controlled h2) { margin: 0; }
:where(.command-palette-controlled [role="group"]) {
display: flex;
flex-wrap: wrap;
gap: 0.625rem;
align-items: center;
}
"""
preview = ControlledCommandPalette()
preview # noqa: B018
A completed close clears the uncontrolled query exactly once. A declined controlled close preserves it. Releasing a controlled value with null or by omitting it continues from the last accepted fallback, never rejected browser text or the original server seed.
Choose action and close policy
onAction(value, detail) runs synchronously before an optional close request. The root close_on_action default can be overridden by one command. Callback return values are ignored. If the callback throws, the close step does not run. If it deliberately moves focus, that connected focus destination wins over Dialog return-focus behavior.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class CommandActions(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteCommand(
value="copy-id",
label="Copy ID",
close_on_action=False,
),
CCommandPaletteCommand(
value="toggle-sidebar",
label="Toggle sidebar",
close_on_action=False,
),
CCommandPaletteCommand(
value="delete-draft",
label="Delete draft",
intent="danger",
),
)
}
template = """
<section
class="command-palette-actions"
x-data="{open:true,events:[],throwNext:false,moveFocus:false}"
>
<h2>Action transaction</h2>
<div role="group" aria-label="Action behavior">
<label><input type="checkbox" x-model="throwNext" /> Throw in next action</label>
<label><input type="checkbox" x-model="moveFocus" /> Move owner focus</label>
</div>
<button id="command-action-focus-target" type="button">Owner focus target</button>
<c-CCommandPalette
label="Draft commands"
c-entries="commands"
$c-props="{
open,
onOpenChange:(value,detail)=>{
events.push(`open:${value}:${detail.reason}`);
open=value;
},
onQueryChange:(value,detail)=>events.push(`query:${value}:${detail.reason}`),
onAction:(value,detail)=>{
events.push(`action:${value}:${detail.source}:${detail.closeOnAction}`);
if (moveFocus) document.getElementById('command-action-focus-target').focus();
if (throwNext) { throwNext=false; throw new Error('Application action failed'); }
},
}"
/>
<output aria-live="polite" x-text="events.slice(-4).join(' | ') || 'No actions yet'">
No actions yet
</output>
</section>
"""
css = """
:where(.command-palette-actions) {
display: grid;
gap: 0.75rem;
justify-items: start;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-actions h2) { margin: 0; }
:where(.command-palette-actions [role="group"]) {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
"""
preview = CommandActions()
preview # noqa: B018
Own global shortcuts in the application
CommandPalette installs no document or window shortcut listener. The application decides how Mod+K behaves around editable controls, composition, multiple palettes, operating-system reservations, and shortcut collisions, then updates controlled open.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class ApplicationShortcut(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"help_commands": (
CCommandPaletteCommand(value="help-docs", label="Open documentation"),
CCommandPaletteCommand(value="help-support", label="Contact support"),
),
"workspace_commands": (
CCommandPaletteCommand(value="workspace-settings", label="Open workspace settings"),
CCommandPaletteCommand(value="workspace-members", label="Manage workspace members"),
),
}
template = """
<section
class="command-palette-shortcut"
x-data="{workspaceOpen:false,helpOpen:false,enabled:true,target:'workspace',opens:0}"
@keydown.window="
enabled
&& ($event.metaKey || $event.ctrlKey)
&& $event.key.toLowerCase()==='k'
&& !$event.isComposing
&& !['INPUT','TEXTAREA','SELECT'].includes($event.target.tagName)
&& !$event.target.isContentEditable
&& (
$event.preventDefault(),
opens++,
target==='workspace' ? workspaceOpen=true : helpOpen=true
)
"
>
<h2>Application-owned Mod+K</h2>
<p>Focus the app shell and press Mod+K. Editable targets stay native.</p>
<label><input type="checkbox" x-model="enabled" /> Enable app shortcut</label>
<label>
Shortcut target
<select x-model="target">
<option value="workspace">Workspace palette</option>
<option value="help">Help palette</option>
</select>
</label>
<label>Unrelated input <input type="text" value="Mod+K stays editable here" /></label>
<div contenteditable="true" role="textbox" aria-label="Editable application note">
Contenteditable shortcut exclusion
</div>
<c-CCommandPalette
label="Workspace commands"
c-entries="workspace_commands"
$c-props="{
open:workspaceOpen,
onOpenChange:(value)=>workspaceOpen=value,
}"
/>
<c-CCommandPalette
label="Help commands"
c-entries="help_commands"
$c-props="{
open:helpOpen,
onOpenChange:(value)=>helpOpen=value,
}"
/>
<output>Handled app shortcuts: <span x-text="opens">0</span></output>
</section>
"""
css = """
:where(.command-palette-shortcut) {
display: grid;
gap: 0.75rem;
max-inline-size: 42rem;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-shortcut h2, .command-palette-shortcut p) { margin: 0; }
:where(.command-palette-shortcut [contenteditable]) {
min-block-size: 2.75rem;
padding: 0.625rem;
border: 1px solid currentColor;
}
"""
preview = ApplicationShortcut()
preview # noqa: B018
Shortcut text inside a command is a hint, not a binding or authorization rule.
Keep Forms and IME input safe
The search input has no name, value contribution, reset behavior, or validity. Every noncomposing Enter is contained before an ancestor Form can submit, including empty and all-disabled results. During composition, Arrow, Enter, and Escape remain with the IME and cannot navigate, act, clear, or dismiss. The final committed text produces at most one query request.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class FormSafePalette(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteCommand(value="focus-name", label="Focus display name"),
CCommandPaletteCommand(value="submit-profile", label="Submit profile explicitly"),
CCommandPaletteCommand(value="managed-setting", label="Managed setting", disabled=True),
)
}
template = """
<form
id="command-palette-profile-form"
class="command-palette-form"
x-data="{open:false,submits:0,actions:0,query:''}"
@submit.prevent="submits++"
>
<h2>Profile Form</h2>
<label>Display name <input id="command-profile-name" name="display_name" value="Ada" /></label>
<c-CCommandPalette
label="Profile commands"
c-entries="commands"
$c-props="{
open,
query,
onOpenChange:(value)=>open=value,
onQueryChange:(value)=>query=value,
onAction:(value)=>{
actions++;
if (value==='focus-name') document.getElementById('command-profile-name').focus();
if (value==='submit-profile') {
document.getElementById('command-palette-profile-form').requestSubmit();
}
},
}"
>
<c-fill name="activator" data="{ activator_attrs, activator_disabled }">
<c-CButton
type="button"
c-disabled="activator_disabled"
c-attrs="activator_attrs"
>Open profile commands</c-CButton>
</c-fill>
</c-CCommandPalette>
<button type="submit">Save profile</button>
<output aria-live="polite">
Native submits: <span x-text="submits">0</span>;
palette actions: <span x-text="actions">0</span>
</output>
<p>
IME fixture: composition Enter and Escape remain native; ordinary
palette Enter never submits this Form unless the action callback asks.
</p>
</form>
"""
css = """
:where(.command-palette-form) {
display: grid;
gap: 0.75rem;
justify-items: start;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-form h2, .command-palette-form p) { margin: 0; }
:where(.command-palette-form label) { display: grid; gap: 0.25rem; }
"""
preview = FormSafePalette()
preview # noqa: B018
An action callback may explicitly submit application data. The palette itself never calls requestSubmit() or changes FormData.
Compose with modal and anchored layers
CommandPalette uses the same native Dialog controller as CDialog. A nested Dialog becomes the topmost focus owner. Popovers opened from a command close before the palette. Escape closes only the deepest owned layer, and ordinary close restores the eligible deep-focus invoker.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand
citry.register_library(citry_ui)
class PaletteLayers(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteCommand(
value="show-details",
label="Show deployment details",
close_on_action=False,
),
CCommandPaletteCommand(value="close-workflow", label="Finish workflow"),
)
}
template = """
<section
class="command-palette-layers"
x-data="{removed:false}"
x-init="
Alpine.store('commandPaletteLayers', {paletteOpen:false,popoverOpen:false});
$nextTick(() => {
const host=$refs.shadowHost;
const fixture=$refs.shadowFixture;
if (!host.shadowRoot && fixture) {
Alpine.destroyTree(fixture);
host.attachShadow({mode:'open'}).append(fixture);
Alpine.initTree(fixture);
}
})
"
>
<h2>Modal and anchored layers</h2>
<c-CDialog>
<c-fill name="activator" data="{ activator_attrs }">
<c-CButton c-attrs="activator_attrs">Open deployment workflow</c-CButton>
</c-fill>
<c-fill name="title">Deployment workflow</c-fill>
<c-fill name="default">
<div class="command-palette-layers__workflow">
<div x-ref="paletteOwner">
<c-CCommandPalette
label="Deployment workflow commands"
c-entries="commands"
$c-props="{
open:$store.commandPaletteLayers.paletteOpen,
onOpenChange:(value)=>$store.commandPaletteLayers.paletteOpen=value,
onAction:(value)=>{
if (value==='show-details') $store.commandPaletteLayers.popoverOpen=true;
},
}"
>
<c-fill name="activator" data="{ activator_attrs, activator_disabled }">
<c-CButton
c-disabled="activator_disabled"
c-attrs="activator_attrs"
>Open workflow commands</c-CButton>
</c-fill>
</c-CCommandPalette>
</div>
<c-CPopover
$c-props="{
open:$store.commandPaletteLayers.popoverOpen,
onOpenChange:(value)=>$store.commandPaletteLayers.popoverOpen=value,
}"
>
<c-fill name="activator" data="{ activator_attrs }">
<c-CButton variant="outline" c-attrs="activator_attrs">Details anchor</c-CButton>
</c-fill>
<c-fill name="title">Deployment details</c-fill>
<c-fill name="default">The latest deployment passed its checks.</c-fill>
</c-CPopover>
<button
type="button"
@click="$refs.paletteOwner.remove(); removed=true"
x-show="!removed"
>Remove palette owner</button>
<output x-text="removed ? 'Palette owner removed' : 'Palette owner present'">
Palette owner present
</output>
</div>
</c-fill>
</c-CDialog>
<div x-ref="shadowHost" class="command-palette-layers__shadow-host">
<div x-ref="shadowFixture">
<c-CCommandPalette label="ShadowRoot commands" c-entries="commands">
<c-fill name="activator" data="{ activator_attrs, activator_disabled }">
<c-CButton
c-disabled="activator_disabled"
c-attrs="activator_attrs"
>Open ShadowRoot fixture</c-CButton>
</c-fill>
</c-CCommandPalette>
</div>
</div>
</section>
"""
css = """
:where(.command-palette-layers) {
display: grid;
gap: 0.75rem;
justify-items: start;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-layers h2) { margin: 0; }
:where(.command-palette-layers__workflow) {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
align-items: center;
}
:where(.command-palette-layers__shadow-host) {
display: block;
padding: 0.75rem;
border: 1px solid currentColor;
}
"""
preview = PaletteLayers()
preview # noqa: B018
The Dialog stays in its authored Document or open ShadowRoot. Closed ShadowRoots, cross-document adoption, invalid anatomy, and hostile ownership changes fail closed.
Adapt size, direction, and environment
size coordinates surface width, input height, and row density. Public variables and part selectors support application styling. Logical layout keeps start/end decoration correct in RTL, while vertical command order remains unchanged.
Show code
from typing import Any
import citry_ui
from citry import Component, citry
from citry_ui import CCommandPaletteCommand, CCommandPaletteGroup
citry.register_library(citry_ui)
class CommandPaletteEnvironment(Component):
def template_data(self, kwargs: Any, slots: Any) -> dict[str, object]: # noqa: ARG002
return {
"commands": (
CCommandPaletteGroup(
label="Localized workspace administration",
commands=(
CCommandPaletteCommand(
value="archive-workspace",
label="Archive this exceptionally long localized workspace name",
description="Keeps a recoverable copy for organization administrators",
),
CCommandPaletteCommand(
value="delete-workspace",
label="Delete workspace permanently",
description="This command cannot be undone",
intent="danger",
),
CCommandPaletteCommand(
value="managed-workspace",
label="Transfer managed workspace",
disabled=True,
),
),
),
)
}
template = """
<section
class="command-palette-environment"
x-data="{open:true,size:'md',dark:false,rtl:false}"
:class="dark ? 'command-palette-environment--dark' : ''"
:dir="rtl ? 'rtl' : 'ltr'"
>
<h2>Responsive command environment</h2>
<div role="group" aria-label="Environment controls">
<label>
Size
<select x-model="size">
<option value="sm">Small</option>
<option value="md">Medium</option>
<option value="lg">Large</option>
</select>
</label>
<label><input type="checkbox" x-model="dark" /> Dark scheme</label>
<label><input type="checkbox" x-model="rtl" /> RTL</label>
</div>
<c-CCommandPalette
label="Localized workspace commands"
c-entries="commands"
c-style="{
'--cui-command-palette-inline-size':'min(42rem, calc(100dvi - 1rem))',
'--cui-command-palette-row-min-block-size':'3rem',
}"
$c-props="{
open,
size,
onOpenChange:(value)=>open=value,
}"
/>
<p>
Inspect sm, md, and lg at 200% and 400% zoom, narrow and wide widths,
coarse pointer, virtual keyboard, reduced motion, forced colors, and print.
</p>
</section>
"""
css = """
:where(.command-palette-environment) {
display: grid;
gap: 0.75rem;
color: CanvasText;
color-scheme: light;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.command-palette-environment--dark) {
color-scheme: dark;
background: Canvas;
}
:where(.command-palette-environment h2, .command-palette-environment p) { margin: 0; }
:where(.command-palette-environment [role="group"]) {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
@media (forced-colors: active) {
:where(.command-palette-environment) { border: 1px solid CanvasText; }
}
@media print {
:where(.command-palette-environment [role="group"]) { display: none; }
}
"""
preview = CommandPaletteEnvironment()
preview # noqa: B018
The active option stays visible at narrow widths, 200% and 400% zoom, with a virtual keyboard, coarse pointer, text spacing, reduced motion, and forced colors. The modal palette is hidden in print.
Without JavaScript, a server-closed palette stays closed. A server-open native Dialog remains readable in document flow without claiming modality. Its search input remains disabled and commands do not run, so it cannot submit an ancestor Form or promise unavailable interaction.
Distinguish callbacks from native events
onOpenChange, onQueryChange, and onAction are component callbacks passed through $c-props. Native input, composition, keyboard, pointer, click, Dialog cancel, and close events remain browser events. The family dispatches no custom DOM event.
attrs target the native Dialog. input_attrs target the owned search input and accept only attributes that cannot replace its identity, value, disabled state, Form boundary, combobox relationships, or active descendant. Mappings are copied once. Labels, descriptions, keywords, shortcut hints, and values are escaped text, not HTML or authorized domain actions.
API reference
Inputs
CCommandPalette server inputs
Server inputs are passed in a template through <c-CCommandPalette ... /> or in Python through CCommandPalette(...).
| Input | Type | Default | Effect |
|---|---|---|---|
entries | Sequence[CCommandPaletteEntry] (CCommandPaletteEntry) | required | Snapshots and validates ordered command, group, and separator records. |
label | non-whitespace str | required | Supplies the visible Dialog title and accessible name. |
id | str | None | generated | Sets the Dialog identity and bases owned relationship IDs. |
open | bool | False | Selects initial server and uncontrolled Dialog visibility. |
query | str | "" | Seeds the exact search text without server-side filtering. |
disabled | bool | False | Disables activation and force-closes an open palette. |
loop | bool | True | Wraps active Arrow navigation at the first and last eligible command. |
close_on_action | bool | True | Sets the root action-close default that each command may override. |
size | "sm" | "md" | "lg" (CCommandPaletteSize) | "md" | Selects coordinated surface width and control density. |
placeholder | str | "Search commands" | Supplies visible search-input placeholder text. |
search_label | non-whitespace str | "Search commands" | Supplies the visually hidden native label for the search input. |
empty_label | non-whitespace str | "No commands found" | Supplies the empty live-status fallback when the empty slot is omitted. |
close_label | non-whitespace str | "Close command palette" | Supplies the built-in close Button accessible name. |
class_ | CClassValue | None (CClassValue) | None | Adds classes to the native Dialog and merges them with attrs. |
style | CStyleValue | None (CStyleValue) | None | Adds styles to the native Dialog and merges them with attrs. |
attrs | Mapping[str, object] | None | None | Adds copied allowed native Dialog attributes without replacing owned semantics or state. |
input_attrs | Mapping[str, object] | None | None | Adds copied allowed search-input attributes without replacing Form, value, focus, or ARIA ownership. |
CCommandPalette client inputs
Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CCommandPalette />.
| Input | Type | Omitted behavior | Effect |
|---|---|---|---|
open | boolean | null | Releases control from committed visibility; null has the same effect. | Controls native Dialog visibility while supplied as a Boolean. |
query | string | null | Releases control from the last accepted internal query fallback; null has the same effect. | Controls exact input text and filtering while supplied as a string. |
disabled | boolean | Uses the immutable server input. | Controls activation and forced closure. |
loop | boolean | Uses the immutable server input. | Controls Arrow navigation wrapping. |
closeOnAction | boolean | Uses the immutable server input. | Controls the root action-close default. |
size | "sm" | "md" | "lg" (CCommandPaletteSize) | Uses the immutable server input. | Controls coordinated surface width and density. |
onOpenChange | function | Omission selects no visibility callback; null clears the last valid callback. | Receives user-authored and forced visibility requests. |
onQueryChange | function | Omission selects no query callback; null clears the last valid callback. | Receives committed user input and accepted-close reset requests. |
onAction | function | Omission selects no command callback; null clears the last valid callback. | Receives one eligible command activation before optional close. |
Slots
Slots are passed as nested content or <c-fill> tags in a template, or through the slots={...} argument in Python.
CCommandPalette slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
activator | no | {activator_attrs: dict[str, object], activator_disabled: bool} | None. Bind the complete mapping to one ordinary native activator; for CButton also bind activator_disabled through disabled. |
item_start | no | CCommandPaletteItemSlotData (CCommandPaletteItemSlotData) | None. Output is inert and accessibility-hidden visual decoration. |
item_end | no | CCommandPaletteItemSlotData (CCommandPaletteItemSlotData) | Escaped shortcut text when supplied. Output is inert and accessibility-hidden. |
empty | no | {} | Escaped empty_label text. Output is inert and cannot contain interactive content. |
Events
Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.
CCommandPalette events
| Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior |
|---|---|---|---|---|
onOpenChange | (requestedOpen: boolean, detail: CCommandPaletteOpenChangeDetail) => void (CCommandPaletteOpenChangeDetail) | Activator, Escape, outside dismissal, close Button, action, native close, disabled transition, ancestor close, or owner request changes visibility. | {reason, controlled, source} (CCommandPaletteOpenChangeDetail) | Uncontrolled state commits before notification. Controlled state remains authoritative and may decline an ordinary close by retaining true. |
onQueryChange | (requestedQuery: string, detail: CCommandPaletteQueryChangeDetail) => void (CCommandPaletteQueryChangeDetail) | A noncomposing user edit settles or an accepted close clears a nonempty query. | {reason, closeReason, controlled, source} (CCommandPaletteQueryChangeDetail) | Controlled input is request-only and restores every observable surface when the owner declines. Accepted close clears the internal fallback once. |
onAction | (value: string, detail: CCommandPaletteActionDetail) => void (CCommandPaletteActionDetail) | An enabled visible active command receives unmodified Enter or an eligible option receives a plain click. | {query, source, item, event, closeOnAction} (CCommandPaletteActionDetail) | Runs synchronously before optional close; return values are ignored and an exception stops the close transaction. |
Methods
-
CSS
CSS variables to theme the components. Set them on an ancestor or the component itself.
CCommandPalette CSS variables
Apply these variables to CCommandPalette or one of its ancestors.
| Variable | Type | Purpose | Default |
|---|---|---|---|
--cui-command-palette-backdrop | color | Native modal backdrop. | Theme overlay color. |
--cui-command-palette-background | color | Dialog surface background. | Theme surface color. |
--cui-command-palette-foreground | color | Primary text. | Theme foreground. |
--cui-command-palette-muted | color | Descriptions and shortcut hints. | Theme muted foreground. |
--cui-command-palette-border-color | color | Surface, input, and row boundaries. | Theme border color. |
--cui-command-palette-active-background | color | Active option background. | Theme subtle accent. |
--cui-command-palette-active-foreground | color | Active option text. | Theme accent foreground. |
--cui-command-palette-danger | color | Danger command text. | Theme danger color. |
--cui-command-palette-radius | length | Surface corner radius. | 0.875rem |
--cui-command-palette-shadow | shadow | Modal elevation. | Theme overlay shadow. |
--cui-command-palette-inline-size | length | Preferred Dialog width. | Size-derived. |
--cui-command-palette-max-block-size | length | Viewport-constrained Dialog height. | calc(100dvb - 2rem) |
--cui-command-palette-padding | length | Outer surface spacing. | 0.75rem |
--cui-command-palette-gap | length | Gap between surface regions. | 0.5rem |
--cui-command-palette-input-block-size | length | Search-control height. | Size-derived. |
--cui-command-palette-row-min-block-size | length | Command row minimum height. | Size-derived; at least 2.75rem. |
--cui-command-palette-row-padding-inline | length | Command row horizontal inset. | 0.75rem |
--cui-command-palette-group-gap | length | Spacing between command groups. | 0.5rem |
--cui-command-palette-focus-ring | color | Visible keyboard focus ring. | Theme focus color. |
Attributes
HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.
CCommandPalette attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
id | Native Dialog | supplied or generated string | Identifies the palette and bases all owned relationships. |
open | Native Dialog | present | absent | Native Dialog visibility; enhanced open uses showModal. |
data-open | Native Dialog | present | absent | Mirrors effective committed visibility. |
data-disabled | Native Dialog | present | absent | Mirrors effective palette disabledness. |
data-size | Native Dialog | "sm" | "md" | "lg" (CCommandPaletteSize) | Mirrors effective surface width and density. |
data-empty | Native Dialog | present | absent | Mirrors whether filtering exposes zero command results. |
aria-labelledby | Native Dialog | owned title IDREF | Names the modal from its visible title. |
CCommandPalette attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
type | Search input | "text" | Avoids divergent native search Escape and clear behavior. |
role | Search input | "combobox" | Exposes editable command filtering. |
aria-autocomplete | Search input | "list" | Announces list filtering without completing the input value. |
aria-controls | Search input | owned listbox IDREF | References the result collection. |
aria-expanded | Search input | "true" | "false" | Mirrors effective result-surface visibility. |
aria-activedescendant | Search input | eligible owned option IDREF | absent | Exposes internal active navigation while DOM focus stays in the input. |
disabled | Search input | present | absent | Keeps server fallback and effective disabled state natively safe. |
CCommandPalette attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
role | Command row | "option" | Exposes one callback-only command candidate. |
aria-selected | Command row | "true" | "false" | Mirrors transient active-descendant state rather than an application value. |
aria-disabled | Command row | "true" | absent | Exposes an unavailable command. |
data-active | Command row | present | absent | Mirrors internal active state. |
data-disabled | Command row | present | absent | Mirrors immutable command disabledness. |
data-intent | Command row | "default" | "danger" (CCommandPaletteIntent) | Mirrors immutable visual intent. |
CCommandPalette attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
role | Command group | "group" | Groups commands under one visible label. |
aria-labelledby | Command group | owned group-label IDREF | Names the group from its visible label. |
Selectors
Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.
CCommandPalette selectors
| Selector | Element | Purpose |
|---|---|---|
[data-citry-ui-part="command-palette"] | Native Dialog | Modal owner and class_, style, and attrs destination. |
[data-citry-ui-part="command-palette-surface"] | Surface section | Contains every visual palette region. |
[data-citry-ui-part="command-palette-header"] | Header | Lays out the title and close Button. |
[data-citry-ui-part="command-palette-title"] | Heading | Provides visible Dialog name. |
[data-citry-ui-part="command-palette-close"] | Button | Closes the current palette through shared Dialog policy. |
[data-citry-ui-part="command-palette-search"] | Search landmark | Owns the native label and editable combobox. |
[data-citry-ui-part="command-palette-search-label"] | Native label | Supplies the search input accessible name. |
[data-citry-ui-part="command-palette-input"] | Text input | Owns query editing and active-descendant navigation. |
[data-citry-ui-part="command-palette-listbox"] | Listbox | Owns visible command options and labelled groups. |
[data-citry-ui-part="command-palette-command"] | Option row | Shows one callback-only command and its state. |
[data-citry-ui-part="command-palette-group"] | Group section | Groups visible command options. |
[data-citry-ui-part="command-palette-group-label"] | Group label | Names one visible group. |
[data-citry-ui-part="command-palette-separator"] | Accessibility-hidden hr | Separates visible top-level regions. |
[data-citry-ui-part="command-palette-empty"] | Live status | Announces and displays the empty result. |
[data-citry-ui-part="command-palette-item-start"] | Inert leading wrapper | Displays accessibility-hidden visual decoration. |
[data-citry-ui-part="command-palette-item-end"] | Inert trailing wrapper | Displays accessibility-hidden decoration or shortcut text. |
Interfaces
Aliases and data shapes referenced above.
Input type aliases
| Interface | Definition |
|---|---|
CClassValue | str | Mapping[str, bool] | Sequence[CClassValue] |
CStyleValue | str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] |
CCommandPaletteEntry | CCommandPaletteCommand | CCommandPaletteGroup | CCommandPaletteSeparator |
CCommandPaletteIntent | Literal["default", "danger"] |
CCommandPaletteSize | Literal["sm", "md", "lg"] |
CCommandPaletteActionSource | Literal["keyboard", "click"] |
CCommandPaletteOpenReason | Literal["trigger", "escape", "outside", "close-button", "action", "native", "disabled", "ancestor", "owner"] |
CCommandPaletteQueryReason | Literal["input", "close"] |
CCommandPaletteCommand
| Field | Type | Default | Meaning |
|---|---|---|---|
value | non-whitespace str | - | Globally unique opaque application command identity. |
label | non-whitespace str | - | Visible owned command label and accessible name. |
description | str | None | - | Optional visible owned supporting description. |
keywords | tuple[str, ...] | - | Immutable search-only aliases; default is empty. |
shortcut | str | None | - | Optional accessibility-hidden visual hint with no listener; default is null. |
disabled | bool | - | Immutable unavailable state; default is false. |
close_on_action | bool | None | - | Optional per-command close override; null uses the root policy. |
intent | CCommandPaletteIntent (CCommandPaletteIntent) | - | Visual default or danger emphasis; default is default. |
CCommandPaletteGroup
| Field | Type | Default | Meaning |
|---|---|---|---|
label | non-whitespace str | - | Visible accessible group label. |
commands | tuple[CCommandPaletteCommand, ...] | - | Nonempty immutable command tuple; groups never nest. |
CCommandPaletteSeparator
Empty dataclass: {}.
CCommandPaletteItemSlotData
| Field | Type | Default | Meaning |
|---|---|---|---|
value | str | - | Stable command identity. |
label | str | - | Owned command label. |
description | str | None | - | Optional owned description. |
keywords | tuple[str, ...] | - | Immutable search aliases. |
shortcut | str | None | - | Optional visual shortcut hint. |
disabled | bool | - | Immutable command disabledness. |
close_on_action | bool | - | Effective command close policy after the root fallback. |
intent | CCommandPaletteIntent (CCommandPaletteIntent) | - | Immutable visual intent. |
CCommandPaletteOpenChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | CCommandPaletteOpenReason (CCommandPaletteOpenReason) | - | Cause of the requested or committed visibility change. |
controlled | boolean | - | Whether a valid client Boolean owns desired visibility. |
source | object | null | - | Connected owned origin when one remains available. |
CCommandPaletteQueryChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | CCommandPaletteQueryReason (CCommandPaletteQueryReason) | - | User input or accepted-close reset. |
closeReason | CCommandPaletteOpenReason | null (CCommandPaletteOpenReason) | - | Accepted close cause for reset; null for ordinary input. |
controlled | boolean | - | Whether a valid client string owns effective query text. |
source | object | null | - | Owned input or accepted close origin when available. |
CCommandPaletteActionDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
query | string | - | Exact accepted effective query at activation time. |
source | CCommandPaletteActionSource (CCommandPaletteActionSource) | - | Keyboard Enter or accepted click-handler path. |
item | object | - | Exact owned option Element. |
event | object | - | Triggering native browser event. |
closeOnAction | boolean | - | Effective close policy for this action. |
Translation keys
Catalog keys used by this family. An explicit component input or slot listed in Override takes precedence over the catalog for that instance.
CCommandPalette translation keys
| Key | Purpose | Variables | Override | Browser updates |
|---|---|---|---|---|
citry-ui-command-palette-placeholder | Provides the search-field hint. | None | placeholder input | $c-tr updates placeholder. |
citry-ui-command-palette-search-label | Labels the command search field. | None | search_label input | $c-tr updates text content. |
citry-ui-command-palette-empty | Reports that no commands match. | None | empty_label input or empty slot | $c-tr updates fallback text. |
citry-ui-command-palette-close | Names the palette close control. | None | close_label input | $c-tr updates aria-label. |