CommandPalette

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.

Examples

Run a command

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>
  );
}

Show recent choices first

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>
  );
}

Open it with a shortcut

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>
  );
}

Find a command by its synonyms

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>
  );
}

Search hundreds of pages

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>
  );
}

When to use something else

Keyboard

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.

API

CommandPaletteProps

NameTypeDefaultDescription
openrequiredboolean—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

itemsrequiredreadonly 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

restingItemsreadonly 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

maxFindsnumberevery findThe 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

refRef<HTMLDialogElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Events

NameTypeDefaultDescription
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

NameTypeDefaultDescription
idrequiredstring—What is reported on choosing.

Shown in: Show recent choices first, Find a command by its synonyms, Search hundreds of pages

labelrequiredstring—What is searched and displayed.

Shown in: Show recent choices first, Find a command by its synonyms, Search hundreds of pages

groupstring—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

searchedGroupstringthe whole groupThe 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

iconReactNode—Optional glyph on the left of the row.

Shown in: Search hundreds of pages

weightnumber—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

keywordsreadonly 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

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index