CommandATM
A searchable command palette for quick actions. Built on native <dialog> with search filtering. Open with Cmd+K.
On this page (6)
§Default
Click the button or press ⌘K to open. Use Arrow keys to navigate, Enter to select, Escape to close.
§Density
Set data-density on the component root to scale its internal whitespace. A whitespace policy, not a zoom: only gaps and padding scale (ratio 0.75 / 1 / 1.25), typography and fixed dimensions stay identical. comfortable matches the unsized default.
§States
Named states via the shared State API, driven per instance through the bound api:
default- closed (the authored state; trigger orCmd/Ctrl+Kopens it)open- shown modally with the search input focused
The demo element carries data-state-demo; bun run screenshots drives it via el.api.setState(name) and captures screenshots/{mode}/command-{state}.png.
Machine contract - verified against command.schema.json by bun run verify:
| State | Type | Values | Default | Description |
|---|---|---|---|---|
open | boolean | true, false | false | Shown via showModal(); hidden via close() (native open property). |
§API
Generated from command.ts and the shared State API - the descriptions are their JSDoc, the types are checked by the compiler.
States
type CommandState = 'default' | 'open' - setState(name, config) takes the config of the state it names.
| State | Description |
|---|---|
default | Closed. No config. |
open | Open as a modal, its search input focused. No config. |
Every element
| Member | Description | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
el.api.setState<S extends CommandState>(name: S, config?: CommandStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | |||||||||
el.api.getState(): { name: CommandState; config: CommandStateConfigs[CommandState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed. Returns | |||||||||
el.api.render(state?: { name: CommandState; config: CommandStateConfigs[CommandState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | |||||||||
el.api.settled(): Promise<void> | Wait for the last state's DOM work (async states: a diagram rendering, a chart mounting). Returns | |||||||||
el.store: Store<{ name: CommandState; config: CommandStateConfigs[CommandState] }> | A defuss-store store of the element's state - subscribe to follow every change (also the user's), set it to drive the component. |
Registry
| Member | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
df$.shadcn.commandApi.setState<S extends CommandState>(el: HTMLElement, name: S, config?: CommandStateConfigs[S]): unknown | Enter a state: the DOM work runs (also when it is the current state), the store records it.
Returns | ||||||||||||
df$.shadcn.commandApi.getState(el: HTMLElement): { name: CommandState; config: CommandStateConfigs[CommandState]; model?: ElementModel } | The state the element shows now - read back from the DOM, so it includes what the user changed.
Returns | ||||||||||||
df$.shadcn.commandApi.render(state: { name: CommandState; config: CommandStateConfigs[CommandState]; model?: ElementModel }): string | The element's markup in a state - the authored markup with that state applied; a pure function of the state.
Returns | ||||||||||||
df$.shadcn.commandApi.store(el: HTMLElement): Store<{ name: CommandState; config: CommandStateConfigs[CommandState] }> | The element's store (bindComponent made it).
Returns | ||||||||||||
df$.shadcn.commandApi.commit<S extends CommandState>(el: HTMLElement, name: S, config?: CommandStateConfigs[S]): void | Record a state the element reached on its own (no DOM work) - for a component's own handlers.
| ||||||||||||
df$.shadcn.commandStates: CommandState[] | The declared states, 'default' first: default, open. |
§CSS view file
Styles for the command component. Uses design tokens for colors, spacing, and radius.
@layer components { dialog.command { border: none; border-radius: var(--radius-xl); background: var(--popover); color: var(--popover-foreground); padding: 0; max-width: 32rem; width: calc(100% - 2rem); box-shadow: var(--shadow-lg); margin: auto; position: fixed; inset: 0; top: 15%; bottom: auto; overflow: hidden; opacity: 0; transform: scale(0.98); transition: opacity 150ms ease, transform 150ms ease, display 150ms allow-discrete; &[open] { opacity: 1; transform: scale(1); } &::backdrop { background: oklch(0 0 0 / 0); transition: all 150ms ease, display 150ms allow-discrete; } &[open]::backdrop { background: oklch(0 0 0 / 0.45); } } @starting-style { dialog.command[open] { opacity: 0; transform: scale(0.98); } dialog.command[open]::backdrop { background: oklch(0 0 0 / 0); } } /* -- Scroll lock ------------------------------------------- */ /* Page behind stays put while the palette is modal (see dialog.css): `:modal` + overflow:hidden freezes the viewport at its current offset. */ html:has(dialog.command:modal) { overflow: hidden; scrollbar-gutter: stable; } .command-input-wrapper { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); & svg { width: 1rem; height: 1rem; color: var(--muted-foreground); flex-shrink: 0; } } .command-input { flex: 1; border: none; background: transparent; font-size: 0.875rem; color: var(--foreground); outline: none; font-family: inherit; &::placeholder { color: var(--muted-foreground); } } .command-list { max-height: 18rem; overflow-y: auto; overscroll-behavior: contain; padding: 0.25rem; } .command-group { padding: 0.25rem 0; } .command-group-heading { margin: 0; padding: 0.375rem 0.5rem; font-size: 0.75rem; font-weight: 500; color: var(--muted-foreground); } .command-item { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.5rem 0.5rem; border: none; border-radius: var(--radius-md); background: transparent; color: var(--foreground); font-size: 0.8125rem; font-family: inherit; text-align: left; cursor: pointer; outline: none; &:hover, &[data-highlighted] { background-color: var(--accent); } &:focus-visible { outline: 2px solid var(--ring); outline-offset: -2px; } &[aria-disabled="true"] { pointer-events: none; opacity: 0.5; } & svg { width: 1rem; height: 1rem; color: var(--muted-foreground); flex-shrink: 0; } /* Same as combobox-item: `display: flex` beats the UA [hidden] rule, and command.ts filter() hides non-matches via the hidden attribute. */ &[hidden] { display: none; } } .command-separator { height: 1px; background-color: var(--border); margin: 0.25rem -0.25rem; } .command-shortcut { margin-inline-start: auto; font-size: 0.6875rem; color: var(--muted-foreground); font-family: var(--font-mono); } .command-empty { padding: 1.5rem; text-align: center; font-size: 0.875rem; color: var(--muted-foreground); } /* -- Accessibility ------------------------------------------ */ @media (prefers-reduced-motion: reduce) { dialog.command { transition: none; opacity: 1; transform: none; } dialog.command::backdrop { transition: none; } } @media (prefers-contrast: more) { dialog.command { border: 2px solid var(--border); } .command-item { &:hover, &[data-highlighted] { outline: 1px solid var(--foreground); } } } @media (forced-colors: active) { dialog.command { border: 2px solid CanvasText; } .command-item { &:hover, &[data-highlighted] { forced-color-adjust: none; background: Highlight; color: HighlightText; } } .command-separator { background-color: CanvasText; } } /* -- Density ---------------------------------------------------- data-density on the dialog.command root scales the rows of the palette. comfortable == the unsized default. (0,2,1) beats the base (0,1,0). */ dialog.command:where([data-density="compact"]) { & .command-input-wrapper { padding: 0.5rem 0.75rem; } & .command-item { padding: 0.375rem 0.5rem; } & .command-group-heading { padding: 0.25rem 0.5rem; } } dialog.command:where([data-density="comfortable"]) { & .command-input-wrapper { padding: 0.75rem 1rem; } & .command-item { padding: 0.5rem 0.5rem; } & .command-group-heading { padding: 0.375rem 0.5rem; } } dialog.command:where([data-density="spacious"]) { & .command-input-wrapper { padding: 1rem 1.25rem; } & .command-item { padding: 0.625rem 0.5rem; } & .command-group-heading { padding: 0.5rem 0.5rem; } }}§JavaScript view file
Interaction logic for the command component. Uses data attributes for wiring.
// -- Command --------------------------------------------------// Command palette dialog with search filtering, keyboard navigation, and// Cmd/Ctrl+K shortcut, plus the named-state API so agents/tests can drive// open/closed by name (AGENTS.md "State API").// Shared preamble (AGENTS.md "State API"); the implementation lives in core.js —// build.ts rewrites this import into a df$.shadcn.shared binding in dist/.// defussQuery: the callable runtime - filtering flags + the move-highlight// marker ride query scalar writes; membership stays authored (flag-based// filtering, no renderer - §3 command row: keyed morph only once a data// source drives the result set, which the docs palette may adopt later).import { defussGlobals, defussQuery, componentState, bindComponent, bindGlobalKeys } from '../../../shared/state-api.js';const df$ = defussGlobals();const dfDollar = defussQuery();const commandStates = ['default', 'open'];// VERIFIED: (verify's API docs gate) the states below are exactly the declared ones, each// described, and every config field typed, described and named in the code./** setState() configs per state - the command palette's states take none. */export interface CommandStateConfigs { /** Closed. */ default: {}; /** Open as a modal, its search input focused. */ open: {};}/** * The markup of a state: 'open' carries `open`. render() applies it to a * detached copy; on the live element showModal()/close() (the native * protocol: top layer, focus, inert background) produce exactly this * attribute - the e2e render round trip proves they agree. */function applyMarkup(el, stateName) { dfDollar(el).attr('open', stateName === 'open' ? '' : null);}/** * UI side of setState: 'default' closes, 'open' opens modally and focuses * the search input (same affordance as the trigger/keyboard shortcut). */function triggerStateChange(dialog, stateName, _config) { switch (stateName) { case 'default': if (dialog.open) dialog.close(); break; case 'open': if (!dialog.open) dialog.showModal(); { const input = dfDollar(dialog).find<HTMLInputElement>('.command-input').get(0); if (input) input.focus(); } break; }}/** The state a dialog shows: its `open` - read back after every change, and the state it starts in. */const shown = (dialog: HTMLDialogElement) => (dialog.open ? 'open' : 'default');/** Registry-level API; pass the dialog element explicitly. Unknown names throw. */export const commandApi = componentState<HTMLDialogElement>({ component: 'command', states: commandStates, apply: (dialog, state) => triggerStateChange(dialog, state.name, state.config), // the state is the dialog's `open`, as the schema observes it: ⌘K and the // trigger open it natively, without setState, and getState() must see that read: (dialog, state) => ({ name: shown(dialog), config: state.config }), markup: (el, state) => applyMarkup(el, state.name),});df$.commandApi = commandApi;df$.commandStates = commandStates;/* Cmd/Ctrl+K toggles the palette - through the shared global-key listener (src/shared/keys.ts), opted in for editable targets: ⌘K must work while typing in a field (the palette's own input included, where it closes it). */bindGlobalKeys((e) => { if (!(e.metaKey || e.ctrlKey) || e.altKey || e.key.toLowerCase() !== 'k') return; const dialog = dfDollar<HTMLDialogElement>('dialog.command').get(0); if (!dialog) return; e.preventDefault(); if (dialog.open) dialog.close(); else { dialog.showModal(); const input = dfDollar(dialog).find<HTMLInputElement>('.command-input').get(0); if (input) input.focus(); } return true;}, { editable: true });function getVisibleItems(list) { return Array.from(dfDollar(list).find('.command-item:not([hidden]):not([aria-disabled="true"])'));}function highlightItem(list, index) { const visible = getVisibleItems(list); dfDollar(list).find('.command-item[data-highlighted]').data('highlighted', null); if (visible.length === 0) return -1; const clamped = ((index % visible.length) + visible.length) % visible.length; dfDollar(visible[clamped]).data('highlighted', ''); visible[clamped].scrollIntoView({ block: 'nearest' }); // native scroll stays native return clamped;}function init() {dfDollar<HTMLDialogElement>('dialog.command:not([data-init])').toArray().forEach((dialog) => { dialog.dataset.init = ''; // el.store + el.api (AGENTS.md "State through stores") bindComponent(dialog, commandApi, { name: shown(dialog), config: {} }); const input = dfDollar(dialog).find<HTMLInputElement>('.command-input').get(0); const list = dfDollar(dialog).find('.command-list').get(0); const empty = dfDollar(dialog).find('.command-empty').get(0); if (!input || !list) return; let highlightIndex = -1; // query the LIVE list on every filter (KISS): item nodes may be replaced // after init (e.g. the docs palette feeds itself from a generated index), // a cached snapshot would silently keep filtering detached nodes const filter = (q) => { const query = q.toLowerCase(); let hasVisible = false; const $items = dfDollar(list).find('.command-item'); // flag-based filtering through query scalars: nodes keep identity (§3) $items.each(function (this: HTMLElement) { const match = !query || this.textContent.toLowerCase().includes(query); dfDollar(this).prop('hidden', !match); if (match) hasVisible = true; }); dfDollar(list).find('.command-group').each(function (this: HTMLElement) { dfDollar(this).prop('hidden', dfDollar(this).find('.command-item:not([hidden])').length === 0); }); dfDollar(list).find('.command-separator').prop('hidden', !!query); if (empty) dfDollar(empty).prop('hidden', hasVisible); highlightIndex = highlightItem(list, 0); }; input.addEventListener('input', () => { filter(input.value); }); input.addEventListener('keydown', (e) => { const visible = getVisibleItems(list); if (e.key === 'ArrowDown') { e.preventDefault(); highlightIndex = highlightItem(list, highlightIndex + 1); } else if (e.key === 'ArrowUp') { e.preventDefault(); highlightIndex = highlightItem(list, highlightIndex - 1); } else if (e.key === 'Enter') { e.preventDefault(); if (visible[highlightIndex]) { visible[highlightIndex].click(); } } else if (e.key === 'Home') { e.preventDefault(); highlightIndex = highlightItem(list, 0); } else if (e.key === 'End') { e.preventDefault(); highlightIndex = highlightItem(list, visible.length - 1); } }); dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.close(); if ((e.target as HTMLElement).closest('.command-item')) dialog.close(); }); dialog.addEventListener('close', () => { // `close` fires AFTER the exit transition (display allow-discrete), so a // fast re-open (setState/⌘K within 150ms) can beat the queued event - a // stale one must not clear an open palette. (Skipping the reset on // re-open keeps the last query, like macOS Spotlight.) The state itself // follows `open` through read(). if (dialog.open) return; dfDollar(input).val(''); filter(''); dfDollar(list).find('.command-item[data-highlighted]').data('highlighted', null); highlightIndex = -1; }); });dfDollar('[data-command-trigger]:not([data-init])').toArray().forEach((trigger) => { trigger.dataset.init = ''; const dialog = dfDollar<HTMLDialogElement>('#' + CSS.escape(trigger.dataset.commandTrigger)).get(0); if (!dialog) return; trigger.addEventListener('click', () => { dialog.showModal(); const input = dfDollar(dialog).find<HTMLInputElement>('.command-input')[0]; if (input) (input as HTMLElement).focus(); // native focus protocol stays native });});}init();new MutationObserver(init).observe(document, { childList: true, subtree: true });Comments, ideas or improvements? Edit this page's source on GitHub