A search field above the page that finds commands and places as you type (also called a command menu or quick switcher). Reach for it when an application has more actions and pages than any menu can show.
import { CommandPalette, useCommandPaletteShortcut } from "@umriss-ui/core";
It matches by subsequence, not by substring: “ack” finds “Acknowledge the alert”, and the matched letters are marked so an unexpected find explains itself. A candidate's group is searched too.
A candidate is an id, a label and a group; what happens on choosing is yours, so pages and commands live in one list.
Without restingItems the empty palette is only the field. The library keeps no memory: pass recent choices as restingItems and rank them with weight.
Typing finds candidates by subsequence – “ack” finds “Acknowledge the alert”; onChoose reports the id and you decide what happens.
import { useState } from "react";
import { Button, CommandPalette, Stack, Text } from "@umriss-ui/core";
const COMMANDS = [
{ id: "ack", label: "Acknowledge the alert", group: "Incident INC-1048" },
{ id: "page", label: "Page the secondary on call", group: "Incident INC-1048" },
{ id: "status", label: "Post a status update", group: "Incident INC-1048" },
{ id: "resolve", label: "Resolve the incident", group: "Incident INC-1048" },
{ id: "checkout", label: "Open Checkout", group: "Services" },
{ id: "billing", label: "Open Billing", group: "Services" },
{ id: "rota", label: "Show this week's on-call rota", group: "People" },
];
export default function Commands() {
const [open, setOpen] = useState(false);
const [last, setLast] = useState("-");
return (
<Stack gap={3} align="flex-start">
<Button onClick={() => setOpen(true)}>Open the palette</Button>
<Text size="xs" tone="muted">
Last run: {last}
</Text>
<CommandPalette
open={open}
onClose={() => setOpen(false)}
items={COMMANDS}
restingItems={COMMANDS}
onChoose={(id) => {
setOpen(false);
setLast(COMMANDS.find((c) => c.id === id)?.label ?? id);
}}
/>
</Stack>
);
}
restingItems fills the empty palette with the last three choices, and a weight ranks them higher while typing; the memory is yours.
import { useState } from "react";
import { Button, CommandPalette, Stack, Text } from "@umriss-ui/core";
import type { CommandPaletteItem } from "@umriss-ui/core";
const PLACES: CommandPaletteItem[] = [
{ id: "CC-1100", label: "Sales", group: "Cost centres" },
{ id: "CC-1200", label: "Marketing", group: "Cost centres" },
{ id: "CC-2100", label: "Engineering", group: "Cost centres" },
{ id: "CC-3100", label: "Customer service", group: "Cost centres" },
{ id: "CC-4300", label: "IT", group: "Cost centres" },
{ id: "INV-26-0318", label: "INV-26-0318 Brandlow Office Supply", group: "Invoices" },
{ id: "INV-26-0317", label: "INV-26-0317 Kettering & Shaw Events", group: "Invoices" },
];
export default function RecentFirst() {
const [open, setOpen] = useState(false);
const [recent, setRecent] = useState<string[]>(["CC-1200", "INV-26-0317"]);
const items = PLACES.map((place) => (recent.includes(place.id) ? { ...place, weight: 2 } : place));
const resting = recent.map((id) => items.find((item) => item.id === id)!).map((item) => ({ ...item, group: "Recent" }));
return (
<Stack gap={3} align="flex-start">
<Button onClick={() => setOpen(true)}>Go to …</Button>
<Text size="xs" tone="muted">
Recent: {recent.join(", ")}
</Text>
<CommandPalette
open={open}
onClose={() => setOpen(false)}
items={items}
restingItems={resting}
onChoose={(id) => {
setOpen(false);
setRecent((list) => [id, ...list.filter((one) => one !== id)].slice(0, 3));
}}
/>
</Stack>
);
}
useCommandPaletteShortcut binds Cmd-K or Ctrl-K and “/” – except inside a text field, where a slash stays a slash.
import { useCallback, useState } from "react";
import { CommandPalette, Input, Stack, Text, useCommandPaletteShortcut } from "@umriss-ui/core";
const PROJECTS = [
{ id: "shop", label: "Online shop relaunch", group: "Pembury Home" },
{ id: "booking", label: "Booking app", group: "Saltmarsh Clinics" },
{ id: "portal", label: "Member portal", group: "Rowan Credit Union" },
{ id: "intranet", label: "Intranet", group: "Tidewell (internal)" },
];
export default function Shortcut() {
const [open, setOpen] = useState(false);
const [text, setText] = useState("");
const [project, setProject] = useState("-");
useCommandPaletteShortcut(useCallback(() => setOpen(true), []));
return (
<Stack gap={3} align="flex-start">
<Text size="sm" tone="secondary">
Press Cmd-K, Ctrl-K or “/” to jump to a project. Current: {project}
</Text>
<Input
size="sm"
aria-label="Here a slash stays a slash"
value={text}
onChange={(event) => setText(event.target.value)}
placeholder="Type “/” here"
/>
<CommandPalette
open={open}
onClose={() => setOpen(false)}
items={PROJECTS}
onChoose={(id) => {
setOpen(false);
setProject(PROJECTS.find((p) => p.id === id)?.label ?? id);
}}
/>
</Stack>
);
}
keywords lets “snooze” find “Mute the alert for an hour”: they are searched after the label and the group, as a run of three letters or more, and their finds stand last and unmarked.
import { useState } from "react";
import { Button, CommandPalette, Stack, Text } from "@umriss-ui/core";
import type { CommandPaletteItem } from "@umriss-ui/core";
const COMMANDS: CommandPaletteItem[] = [
{ id: "mute", label: "Mute the alert for an hour", group: "Incident INC-1048", keywords: ["snooze", "silence"] },
{ id: "page", label: "Page the secondary on call", group: "Incident INC-1048", keywords: ["escalate", "notify"] },
{ id: "status", label: "Post a status update", group: "Incident INC-1048", keywords: ["announce", "broadcast"] },
{ id: "resolve", label: "Resolve the incident", group: "Incident INC-1048", keywords: ["close", "done"] },
];
export default function Synonyms() {
const [open, setOpen] = useState(false);
const [last, setLast] = useState("-");
return (
<Stack gap={3} align="flex-start">
<Button onClick={() => setOpen(true)}>Find a command</Button>
<Text size="xs" tone="muted">
Try “snooze”, “escalate” or “close”. Last run: {last}
</Text>
<CommandPalette
open={open}
onClose={() => setOpen(false)}
items={COMMANDS}
onChoose={(id) => {
setOpen(false);
setLast(COMMANDS.find((c) => c.id === id)?.label ?? id);
}}
/>
</Stack>
);
}
A row's icon tells a page from an example. searchedGroup searches only “Inputs” of “core · Inputs”, so typing “core” does not find every candidate, and maxFinds draws the first fifty finds and says how many more there are - type “s” to see the line.
import { useState } from "react";
import { AngleGlyph, Button, CommandPalette, GridGlyph, Stack, Text, type CommandPaletteItem } from "@umriss-ui/core";
const PAGES: Record<string, string[]> = {
"core · Inputs": ["Button", "Input", "Textarea", "Select", "Combobox", "MultiSelect", "Checkbox", "Switch", "Slider", "NumberInput"],
"core · Overlays": ["Modal", "Drawer", "Popover", "Tooltip", "Menu", "Toast", "CommandPalette"],
"core · Layout": ["Stack", "Grid", "Card", "Splitter", "Tabs", "Accordion", "Divider"],
"charts · Series": ["Line", "Area", "Bar", "Scatter", "BoxPlot", "Matrix"],
"table · Features": ["Sorting", "Filter", "Grouping", "Selection", "Pagination", "Export"],
};
const EXAMPLES = ["A first one", "Sizes", "States", "With an error", "In a form"];
/* Every page and five examples each: some two hundred candidates. */
const ITEMS = Object.entries(PAGES).flatMap(([group, pages]) => {
const searchedGroup = group.slice(group.indexOf(" · ") + 3);
return pages.flatMap((page): CommandPaletteItem[] => [
{ id: page, label: page, group, searchedGroup, icon: <GridGlyph /> },
...EXAMPLES.map((example) => ({ id: `${page}/${example}`, label: `${page}: ${example}`, group, searchedGroup, icon: <AngleGlyph /> })),
]);
});
export default function SearchHundredsOfPages() {
const [open, setOpen] = useState(false);
const [last, setLast] = useState("-");
return (
<Stack gap={3} align="flex-start">
<Button onClick={() => setOpen(true)}>Search the docs</Button>
<Text size="xs" tone="muted">
{ITEMS.length} candidates · last opened: {last}
</Text>
<CommandPalette
open={open}
onClose={() => setOpen(false)}
items={ITEMS}
maxFinds={50}
onChoose={(id) => {
setOpen(false);
setLast(id);
}}
/>
</Stack>
);
}
| Key | Action |
|---|---|
Cmd+K / Ctrl+K |
Opens the palette, with useCommandPaletteShortcut. |
/ |
Opens the palette too, except while typing in a text field. |
↓ / ↑ |
Moves through the finds while the field keeps the focus. |
Enter |
Chooses the marked find. |
Escape |
Closes the palette. |
CommandPaletteProps| Name | Type | Default | Description |
|---|---|---|---|
openrequired | boolean | — | Controlled like the modal: opening is the caller's decision. Shown in: Run a command, Show recent choices first, Open it with a shortcut and 2 more |
itemsrequired | readonly CommandPaletteItem[] | — | The candidates that are searched. They belong to the caller: the palette
keeps no memory and does not reorder them. Shown in: Run a command, Show recent choices first, Open it with a shortcut and 2 more |
restingItems | readonly CommandPaletteItem[] | — | What stands there as long as nothing is typed. Without a value: nothing,
and the window is only the field.
A LIST and not a switch, because the interesting question is not "all or
none" but "which". Whoever really wants to show everything passes
restingItems={items}; whoever wants to show the five most recently used
passes those - and a switch cannot do that. With long lists "all" is also
exactly the state this component did away with: a list that is already
full can only shrink.
The rows stand in THIS order and without marks: without a query there is
no rank and nothing to mark. Rank and weight therefore play no part here;
the order belongs to the caller.Shown in: Run a command, Show recent choices first |
maxFinds | number | every find | The most finds that stand at once - the best by rank. A last line says how
many more there are, and typing on narrows them. For a long list of
candidates, where a single letter finds hundreds and drawing them all
would make every keystroke wait. Shown in: Search hundreds of pages |
ref | Ref<HTMLDialogElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
| Name | Type | Default | Description |
|---|---|---|---|
onCloserequired | () => void | — | Reports every wish to close - Escape, an outside click, a choice. Shown in: Run a command, Show recent choices first, Open it with a shortcut and 2 more |
onChooserequired | (id: string) => void | — | Reports the id of the chosen candidate. What happens next is the
caller's decision - that is exactly what keeps places and commands
together.Shown in: Run a command, Show recent choices first, Open it with a shortcut and 2 more |
Also takes every attribute of <dialog> – without children, onClose, open.
CommandPaletteItem| Name | Type | Default | Description |
|---|---|---|---|
idrequired | string | — | What is reported on choosing. Shown in: Show recent choices first, Find a command by its synonyms, Search hundreds of pages |
labelrequired | string | — | What is searched and displayed. Shown in: Show recent choices first, Find a command by its synonyms, Search hundreds of pages |
group | string | — | The heading the candidate stands under. It is searched as well. Shown in: Show recent choices first, Find a command by its synonyms, Search hundreds of pages |
searchedGroup | string | the whole group | The part of group that is searched, where not all of it should be: a
prefix every candidate of a set shares - core · before core · Inputs -
would otherwise let its few letters find the whole set.Shown in: Search hundreds of pages |
icon | ReactNode | — | Optional glyph on the left of the row. Shown in: Search hundreds of pages |
weight | number | — | Added to the rank. With it frequency or recency can be expressed - the
library deliberately keeps no memory for that. Shown in: Show recent choices first |
keywords | readonly string[] | — | More words the candidate is found by, such as synonyms - searched but never
shown. Only when neither label nor group matched, only for a query of
three characters or more, and as a contiguous run, not as a subsequence. A
find through them stands behind every find in a label or a group and marks
nothing.Shown in: Find a command by its synonyms |
What umriss deliberately does not build, and why: ADR-0032.