Theme
Version
GitHub PyPI Discord
On this page

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.

Open and run a command
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.

Build command records in Python
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.

Search aliases and empty results
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 disabled commands and shortcut hints
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.

Add safe visual adornments
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.

Control open state and query text
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.

Choose action and close policy
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.

Own a global shortcut in the application
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.

Keep Forms and IME input safe
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.

Compose with modal and anchored layers
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.

Inspect responsive and environment behavior
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(...).

InputTypeDefaultEffect
entriesSequence[CCommandPaletteEntry] (CCommandPaletteEntry)requiredSnapshots and validates ordered command, group, and separator records.
labelnon-whitespace strrequiredSupplies the visible Dialog title and accessible name.
idstr | NonegeneratedSets the Dialog identity and bases owned relationship IDs.
openboolFalseSelects initial server and uncontrolled Dialog visibility.
querystr""Seeds the exact search text without server-side filtering.
disabledboolFalseDisables activation and force-closes an open palette.
loopboolTrueWraps active Arrow navigation at the first and last eligible command.
close_on_actionboolTrueSets the root action-close default that each command may override.
size"sm" | "md" | "lg" (CCommandPaletteSize)"md"Selects coordinated surface width and control density.
placeholderstr"Search commands"Supplies visible search-input placeholder text.
search_labelnon-whitespace str"Search commands"Supplies the visually hidden native label for the search input.
empty_labelnon-whitespace str"No commands found"Supplies the empty live-status fallback when the empty slot is omitted.
close_labelnon-whitespace str"Close command palette"Supplies the built-in close Button accessible name.
class_CClassValue | None (CClassValue)NoneAdds classes to the native Dialog and merges them with attrs.
styleCStyleValue | None (CStyleValue)NoneAdds styles to the native Dialog and merges them with attrs.
attrsMapping[str, object] | NoneNoneAdds copied allowed native Dialog attributes without replacing owned semantics or state.
input_attrsMapping[str, object] | NoneNoneAdds 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 />.

InputTypeOmitted behaviorEffect
openboolean | nullReleases control from committed visibility; null has the same effect.Controls native Dialog visibility while supplied as a Boolean.
querystring | nullReleases control from the last accepted internal query fallback; null has the same effect.Controls exact input text and filtering while supplied as a string.
disabledbooleanUses the immutable server input.Controls activation and forced closure.
loopbooleanUses the immutable server input.Controls Arrow navigation wrapping.
closeOnActionbooleanUses 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.
onOpenChangefunctionOmission selects no visibility callback; null clears the last valid callback.Receives user-authored and forced visibility requests.
onQueryChangefunctionOmission selects no query callback; null clears the last valid callback.Receives committed user input and accepted-close reset requests.
onActionfunctionOmission 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

SlotRequiredDataFallback
activatorno{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_startnoCCommandPaletteItemSlotData (CCommandPaletteItemSlotData)None. Output is inert and accessibility-hidden visual decoration.
item_endnoCCommandPaletteItemSlotData (CCommandPaletteItemSlotData)Escaped shortcut text when supplied. Output is inert and accessibility-hidden.
emptyno{}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

EventSignatureTrigger and timingDetailControlled 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.

VariableTypePurposeDefault
--cui-command-palette-backdropcolorNative modal backdrop.Theme overlay color.
--cui-command-palette-backgroundcolorDialog surface background.Theme surface color.
--cui-command-palette-foregroundcolorPrimary text.Theme foreground.
--cui-command-palette-mutedcolorDescriptions and shortcut hints.Theme muted foreground.
--cui-command-palette-border-colorcolorSurface, input, and row boundaries.Theme border color.
--cui-command-palette-active-backgroundcolorActive option background.Theme subtle accent.
--cui-command-palette-active-foregroundcolorActive option text.Theme accent foreground.
--cui-command-palette-dangercolorDanger command text.Theme danger color.
--cui-command-palette-radiuslengthSurface corner radius.0.875rem
--cui-command-palette-shadowshadowModal elevation.Theme overlay shadow.
--cui-command-palette-inline-sizelengthPreferred Dialog width.Size-derived.
--cui-command-palette-max-block-sizelengthViewport-constrained Dialog height.calc(100dvb - 2rem)
--cui-command-palette-paddinglengthOuter surface spacing.0.75rem
--cui-command-palette-gaplengthGap between surface regions.0.5rem
--cui-command-palette-input-block-sizelengthSearch-control height.Size-derived.
--cui-command-palette-row-min-block-sizelengthCommand row minimum height.Size-derived; at least 2.75rem.
--cui-command-palette-row-padding-inlinelengthCommand row horizontal inset.0.75rem
--cui-command-palette-group-gaplengthSpacing between command groups.0.5rem
--cui-command-palette-focus-ringcolorVisible 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

AttributeElementTypeMeaning
idNative Dialogsupplied or generated stringIdentifies the palette and bases all owned relationships.
openNative Dialogpresent | absentNative Dialog visibility; enhanced open uses showModal.
data-openNative Dialogpresent | absentMirrors effective committed visibility.
data-disabledNative Dialogpresent | absentMirrors effective palette disabledness.
data-sizeNative Dialog"sm" | "md" | "lg" (CCommandPaletteSize)Mirrors effective surface width and density.
data-emptyNative Dialogpresent | absentMirrors whether filtering exposes zero command results.
aria-labelledbyNative Dialogowned title IDREFNames the modal from its visible title.

CCommandPalette attributes

AttributeElementTypeMeaning
typeSearch input"text"Avoids divergent native search Escape and clear behavior.
roleSearch input"combobox"Exposes editable command filtering.
aria-autocompleteSearch input"list"Announces list filtering without completing the input value.
aria-controlsSearch inputowned listbox IDREFReferences the result collection.
aria-expandedSearch input"true" | "false"Mirrors effective result-surface visibility.
aria-activedescendantSearch inputeligible owned option IDREF | absentExposes internal active navigation while DOM focus stays in the input.
disabledSearch inputpresent | absentKeeps server fallback and effective disabled state natively safe.

CCommandPalette attributes

AttributeElementTypeMeaning
roleCommand row"option"Exposes one callback-only command candidate.
aria-selectedCommand row"true" | "false"Mirrors transient active-descendant state rather than an application value.
aria-disabledCommand row"true" | absentExposes an unavailable command.
data-activeCommand rowpresent | absentMirrors internal active state.
data-disabledCommand rowpresent | absentMirrors immutable command disabledness.
data-intentCommand row"default" | "danger" (CCommandPaletteIntent)Mirrors immutable visual intent.

CCommandPalette attributes

AttributeElementTypeMeaning
roleCommand group"group"Groups commands under one visible label.
aria-labelledbyCommand groupowned group-label IDREFNames 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

SelectorElementPurpose
[data-citry-ui-part="command-palette"]Native DialogModal owner and class_, style, and attrs destination.
[data-citry-ui-part="command-palette-surface"]Surface sectionContains every visual palette region.
[data-citry-ui-part="command-palette-header"]HeaderLays out the title and close Button.
[data-citry-ui-part="command-palette-title"]HeadingProvides visible Dialog name.
[data-citry-ui-part="command-palette-close"]ButtonCloses the current palette through shared Dialog policy.
[data-citry-ui-part="command-palette-search"]Search landmarkOwns the native label and editable combobox.
[data-citry-ui-part="command-palette-search-label"]Native labelSupplies the search input accessible name.
[data-citry-ui-part="command-palette-input"]Text inputOwns query editing and active-descendant navigation.
[data-citry-ui-part="command-palette-listbox"]ListboxOwns visible command options and labelled groups.
[data-citry-ui-part="command-palette-command"]Option rowShows one callback-only command and its state.
[data-citry-ui-part="command-palette-group"]Group sectionGroups visible command options.
[data-citry-ui-part="command-palette-group-label"]Group labelNames one visible group.
[data-citry-ui-part="command-palette-separator"]Accessibility-hidden hrSeparates visible top-level regions.
[data-citry-ui-part="command-palette-empty"]Live statusAnnounces and displays the empty result.
[data-citry-ui-part="command-palette-item-start"]Inert leading wrapperDisplays accessibility-hidden visual decoration.
[data-citry-ui-part="command-palette-item-end"]Inert trailing wrapperDisplays accessibility-hidden decoration or shortcut text.

Interfaces

Aliases and data shapes referenced above.

Input type aliases

InterfaceDefinition
CClassValuestr | Mapping[str, bool] | Sequence[CClassValue]
CStyleValuestr | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue]
CCommandPaletteEntryCCommandPaletteCommand | CCommandPaletteGroup | CCommandPaletteSeparator
CCommandPaletteIntentLiteral["default", "danger"]
CCommandPaletteSizeLiteral["sm", "md", "lg"]
CCommandPaletteActionSourceLiteral["keyboard", "click"]
CCommandPaletteOpenReasonLiteral["trigger", "escape", "outside", "close-button", "action", "native", "disabled", "ancestor", "owner"]
CCommandPaletteQueryReasonLiteral["input", "close"]

CCommandPaletteCommand

FieldTypeDefaultMeaning
valuenon-whitespace str-Globally unique opaque application command identity.
labelnon-whitespace str-Visible owned command label and accessible name.
descriptionstr | None-Optional visible owned supporting description.
keywordstuple[str, ...]-Immutable search-only aliases; default is empty.
shortcutstr | None-Optional accessibility-hidden visual hint with no listener; default is null.
disabledbool-Immutable unavailable state; default is false.
close_on_actionbool | None-Optional per-command close override; null uses the root policy.
intentCCommandPaletteIntent (CCommandPaletteIntent)-Visual default or danger emphasis; default is default.

CCommandPaletteGroup

FieldTypeDefaultMeaning
labelnon-whitespace str-Visible accessible group label.
commandstuple[CCommandPaletteCommand, ...]-Nonempty immutable command tuple; groups never nest.

CCommandPaletteSeparator

Empty dataclass: {}.

CCommandPaletteItemSlotData

FieldTypeDefaultMeaning
valuestr-Stable command identity.
labelstr-Owned command label.
descriptionstr | None-Optional owned description.
keywordstuple[str, ...]-Immutable search aliases.
shortcutstr | None-Optional visual shortcut hint.
disabledbool-Immutable command disabledness.
close_on_actionbool-Effective command close policy after the root fallback.
intentCCommandPaletteIntent (CCommandPaletteIntent)-Immutable visual intent.

CCommandPaletteOpenChangeDetail

FieldTypeDefaultMeaning
reasonCCommandPaletteOpenReason (CCommandPaletteOpenReason)-Cause of the requested or committed visibility change.
controlledboolean-Whether a valid client Boolean owns desired visibility.
sourceobject | null-Connected owned origin when one remains available.

CCommandPaletteQueryChangeDetail

FieldTypeDefaultMeaning
reasonCCommandPaletteQueryReason (CCommandPaletteQueryReason)-User input or accepted-close reset.
closeReasonCCommandPaletteOpenReason | null (CCommandPaletteOpenReason)-Accepted close cause for reset; null for ordinary input.
controlledboolean-Whether a valid client string owns effective query text.
sourceobject | null-Owned input or accepted close origin when available.

CCommandPaletteActionDetail

FieldTypeDefaultMeaning
querystring-Exact accepted effective query at activation time.
sourceCCommandPaletteActionSource (CCommandPaletteActionSource)-Keyboard Enter or accepted click-handler path.
itemobject-Exact owned option Element.
eventobject-Triggering native browser event.
closeOnActionboolean-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

KeyPurposeVariablesOverrideBrowser updates
citry-ui-command-palette-placeholderProvides the search-field hint.Noneplaceholder input$c-tr updates placeholder.
citry-ui-command-palette-search-labelLabels the command search field.Nonesearch_label input$c-tr updates text content.
citry-ui-command-palette-emptyReports that no commands match.Noneempty_label input or empty slot$c-tr updates fallback text.
citry-ui-command-palette-closeNames the palette close control.Noneclose_label input$c-tr updates aria-label.