Command
Use the command patch to customize this element.
Customization
Must see the source of patch at the bottom of each patch page to understand the structure then code it still code as html native element.
There are four levels of customization, in increasing order of effort:
- Patch props. Each patch exposes a small, stable set of props—typically fewer than five. Lowest friction.
- Context attributes. Use
dataTone,dataSize, anddataDensityon a container to shift tone, size, or density for an entire subtree without touching individual elements. - Inline override. Native-wins merge strategy: any property set directly on the element overrides the patch value.
- Create a variant. Clone a similar patch and edit it. Use this only when you need a reusable custom version.
Formulas
Unit - U = fontSize / 4 - convert final values with themeSpacing(n).
Size - n = intrinsic text lines, w = wrapping level, d = density factor:
height = (n * 6 + 2 * d * w) * U
paddingBlock = d * w * U
paddingInline = ceil(3 / w) * d * w * U
radius = d * w * U
Base density d = 1.5:
| U | w=0 | w=1 | w=2 | w=3 |
|---|---|---|---|---|
height (n = 1) | 6 | 9 | 12 | 15 |
| paddingBlock | 0 | 1.5 | 3 | 4.5 |
| paddingInline | 3 | 4.5 | 6 | 4.5 |
| radius | 0 | 1.5 | 3 | 4.5 |
Tone - K = N / 2 where N is the palette length. For N = 18, K = 9.
| Role | Shift | n=0 |
|---|---|---|
| Background | parent +/- n | 0 |
| Text | bg + K | 6 |
| Border | bg + K/2 | 3 |
| Hover | bg + 2K/3 | 4 |
| Selected / Focus | above +/- K/3 | 2-4 |
State shift range: K/3 <= delta <= 2K/3.
<div class="blocks">
<div class="block active" data-tab="0">
import { merge, type PartialElement, toState } from "@domphy/core";
import {
type ThemeColor,
themeColor,
themeDensity,
themeSize,
themeSpacing,
} from "@domphy/theme";
/**
* Command-palette container patch. Sets up a vertical flex column and provides a
* shared `command` context (a query State) consumed by `commandSearch` and
* `commandItem` descendants to filter the list. Typically applied to a `<div>`.
*
* @example { div: [...], $: [command()] }
*/
function command(): PartialElement {
return {
_onSchedule: (_node, element) => {
merge(element, {
_context: {
command: {
query: toState(""),
},
},
});
},
style: {
display: "flex",
flexDirection: "column",
overflow: "hidden",
},
};
}
/**
* Search input for a command palette. Wires the input's value into the parent
* `command` context's query State so descendant `commandItem`s filter live.
* Apply to an `<input>` element used inside a `command()`.
*
* @hostTag input
* @param props.color - Base theme color tone. Defaults to "neutral".
* @param props.accentColor - Accent color used for the focus border. Defaults to "primary".
* @example { input: "", $: [commandSearch({ accentColor: "primary" })] }
*/
function commandSearch(
props: { color?: ThemeColor; accentColor?: ThemeColor } = {},
): PartialElement {
const { color = "neutral", accentColor = "primary" } = props;
return {
_onInsert: (node) => {
if (node.tagName !== "input") {
console.warn(`"commandSearch" patch must use input tag`);
}
},
_onMount: (node) => {
const ctx = node.getContext("command");
if (!ctx) {
console.warn(`"commandSearch" patch must be used inside a "command"`);
return;
}
const input = node.domElement as HTMLInputElement;
const onInput = () => ctx.query.set(input.value);
input.addEventListener("input", onInput);
node.addHook("Remove", () => input.removeEventListener("input", onInput));
},
style: {
fontFamily: "inherit",
fontSize: (listener) => themeSize(listener, "inherit"),
paddingInline: (listener) => themeSpacing(themeDensity(listener) * 3),
paddingBlock: (listener) => themeSpacing(themeDensity(listener) * 2),
border: "none",
borderBottom: (listener) =>
`1px solid ${themeColor(listener, "shift-3", color)}`,
outline: "none",
color: (listener) => themeColor(listener, "shift-10", color),
backgroundColor: (listener) => themeColor(listener, "inherit", color),
"&::placeholder": {
color: (listener) => themeColor(listener, "shift-7"),
},
"&:focus-visible": {
borderBottomColor: (listener) =>
themeColor(listener, "shift-6", accentColor),
},
},
};
}
/**
* Selectable item (`role="option"`) in a command palette. Subscribes to the
* parent `command` context's query State and hides itself when its text content
* does not match the current query. Typically applied to a `<button>` (or any
* clickable element) used inside a `command()`.
*
* @param props.color - Base theme color tone. Defaults to "neutral".
* @param props.accentColor - Accent color used for the focus outline. Defaults to "primary".
* @example { button: "Open file", $: [commandItem({ color: "neutral" })] }
*/
function commandItem(
props: { color?: ThemeColor; accentColor?: ThemeColor } = {},
): PartialElement {
const { color = "neutral", accentColor = "primary" } = props;
return {
role: "option",
_onMount: (node) => {
const ctx = node.getContext("command");
if (!ctx) {
console.warn(`"commandItem" patch must be used inside a "command"`);
return;
}
const el = node.domElement as HTMLElement;
const text = el.textContent?.toLowerCase() ?? "";
const release = ctx.query.addListener((q: string) => {
el.hidden = q.length > 0 && !text.includes(q.toLowerCase());
});
node.addHook("Remove", release);
},
style: {
cursor: "pointer",
display: "flex",
alignItems: "center",
width: "100%",
fontSize: (listener) => themeSize(listener, "inherit"),
height: (listener) => themeSpacing(6 + themeDensity(listener) * 2),
paddingInline: (listener) => themeSpacing(themeDensity(listener) * 3),
border: "none",
outline: "none",
color: (listener) => themeColor(listener, "shift-9", color),
backgroundColor: (listener) => themeColor(listener, "inherit", color),
"&:hover:not([disabled])": {
backgroundColor: (listener) => themeColor(listener, "shift-2", color),
},
"&:focus-visible": {
outline: (listener) =>
`${themeSpacing(0.5)} solid ${themeColor(listener, "shift-6", accentColor)}`,
outlineOffset: `-${themeSpacing(0.5)}`,
},
},
};
}
export { command, commandSearch, commandItem };
</div>
</div>