Command Palette
⌘K command palette (Radix Dialog): search input, results filtered as you type and grouped (actions, incidents, schedules), icons and shortcuts, arrow-key highlight, Enter to run, footer hints.
minimal/ui/command-paletteSource
"use client";
import { useEffect, useId, useMemo, useState, type ComponentType, type SVGProps } from "react";
import { Dialog } from "radix-ui";
import { AlarmClock, BellOff, CornerDownLeft, FileText, Plus, Search, Siren, Users } from "lucide-react";
import { cn } from "@/lib/utils";
export interface CommandPaletteItem {
id: string;
label: string;
group: string;
icon?: ComponentType<SVGProps<SVGSVGElement>>;
shortcut?: string;
keywords?: string;
}
export interface CommandPaletteProps {
items?: CommandPaletteItem[];
open?: boolean;
onOpenChange?: (open: boolean) => void;
/** Called with the chosen item; the palette closes afterwards. */
onSelect?: (item: CommandPaletteItem) => void;
/** Open with ⌘K / Ctrl+K. */
hotkey?: boolean;
placeholder?: string;
da?: string;
}
const DEFAULT_ITEMS: CommandPaletteItem[] = [
{ id: "new", label: "Declare incident", group: "Actions", icon: Siren, shortcut: "I" },
{ id: "ack", label: "Acknowledge all my alerts", group: "Actions", icon: Plus, shortcut: "A" },
{ id: "snooze", label: "Snooze notifications for 1 hour", group: "Actions", icon: BellOff },
{ id: "override", label: "Create on-call override", group: "Actions", icon: AlarmClock },
{ id: "inc-2481", label: "INC-2481 · Elevated 5xx on checkout-api", group: "Incidents", icon: FileText, keywords: "checkout payments" },
{ id: "inc-2480", label: "INC-2480 · Webhook delivery delays", group: "Incidents", icon: FileText, keywords: "webhooks eu" },
{ id: "payments", label: "Payments · Primary rotation", group: "Schedules", icon: Users },
{ id: "platform", label: "Platform on-call", group: "Schedules", icon: Users },
];
/** ⌘K command palette (Radix Dialog): filter-as-you-type, grouped results, arrow-key navigation, Enter to run. */
export function CommandPalette({
items = DEFAULT_ITEMS,
open,
onOpenChange,
onSelect,
hotkey = true,
placeholder = "Type a command or search…",
da = "minimal",
}: CommandPaletteProps) {
const [inner, setInner] = useState(false);
const isOpen = open ?? inner;
const [query, setQuery] = useState("");
const [active, setActive] = useState(0);
const listId = useId();
const setOpen = (v: boolean) => {
setInner(v);
onOpenChange?.(v);
if (!v) {
setQuery("");
setActive(0);
}
};
useEffect(() => {
if (!hotkey) return;
const onKey = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
setInner((o) => !o);
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [hotkey]);
const results = useMemo(() => {
const q = query.trim().toLowerCase();
return q ? items.filter((i) => `${i.label} ${i.keywords ?? ""} ${i.group}`.toLowerCase().includes(q)) : items;
}, [items, query]);
const groups = [...new Set(results.map((r) => r.group))];
const run = (item?: CommandPaletteItem) => {
if (!item) return;
onSelect?.(item);
setOpen(false);
};
return (
<Dialog.Root open={isOpen} onOpenChange={setOpen}>
<Dialog.Portal>
<Dialog.Overlay
data-da={da}
className="fixed inset-0 z-50 bg-da-overlay transition-opacity duration-(--da-duration) starting:opacity-0 motion-reduce:transition-none"
/>
<Dialog.Content
data-da={da}
className="da-stroke fixed top-[15vh] left-1/2 z-50 w-[calc(100%-2rem)] max-w-xl -translate-x-1/2 overflow-hidden rounded-da-lg bg-da-surface text-da-surface-fg shadow-da-lg outline-none transition-[opacity,scale] duration-(--da-duration) ease-da starting:scale-[0.98] starting:opacity-0 motion-reduce:transition-none"
>
<Dialog.Title className="sr-only">Command palette</Dialog.Title>
<Dialog.Description className="sr-only">Search commands, incidents and schedules. Use arrow keys and Enter.</Dialog.Description>
<div className="flex items-center gap-2.5 border-b border-da-border px-4">
<Search aria-hidden className="size-4 text-da-muted-fg" />
<input
autoFocus
value={query}
onChange={(e) => {
setQuery(e.target.value);
setActive(0);
}}
onKeyDown={(e) => {
if (e.key === "ArrowDown") {
e.preventDefault();
setActive((a) => Math.min(a + 1, results.length - 1));
} else if (e.key === "ArrowUp") {
e.preventDefault();
setActive((a) => Math.max(a - 1, 0));
} else if (e.key === "Enter") {
e.preventDefault();
run(results[active]);
}
}}
role="combobox"
aria-expanded
aria-controls={listId}
aria-activedescendant={results[active] ? `${listId}-${results[active].id}` : undefined}
aria-label="Search commands"
placeholder={placeholder}
className="h-12 flex-1 bg-transparent text-[15px] outline-none placeholder:text-da-muted-fg"
/>
<kbd className="rounded-[4px] px-1.5 font-da-mono text-[11px] text-da-muted-fg ring-1 ring-da-border">esc</kbd>
</div>
<div id={listId} role="listbox" aria-label="Results" className="max-h-80 overflow-y-auto p-1.5">
{results.length === 0 && <p className="px-3 py-8 text-center text-sm text-da-muted-fg">No results for “{query}”.</p>}
{groups.map((g) => (
<div key={g} role="group" aria-label={g}>
<p aria-hidden className="px-2.5 pt-2 pb-1 text-[11px] font-medium text-da-muted-fg">
{g}
</p>
{results
.filter((r) => r.group === g)
.map((r) => {
const i = results.indexOf(r);
const Icon = r.icon;
return (
<div
key={r.id}
id={`${listId}-${r.id}`}
role="option"
aria-selected={i === active}
onMouseMove={() => setActive(i)}
onClick={() => run(r)}
className={cn("flex cursor-pointer items-center gap-3 rounded-da-md px-2.5 py-2 text-sm", i === active && "bg-da-muted")}
>
{Icon && <Icon aria-hidden className="size-4 text-da-muted-fg" />}
<span className="min-w-0 flex-1 truncate">{r.label}</span>
{r.shortcut && <kbd className="rounded-[4px] px-1.5 font-da-mono text-[11px] text-da-muted-fg ring-1 ring-da-border">{r.shortcut}</kbd>}
{i === active && !r.shortcut && <CornerDownLeft aria-hidden className="size-3.5 text-da-muted-fg" />}
</div>
);
})}
</div>
))}
</div>
<div className="flex items-center gap-4 border-t border-da-border bg-da-surface-2/60 px-4 py-2 text-[11px] text-da-muted-fg">
<span>↑↓ navigate</span>
<span>↵ run</span>
<span className="ml-auto">{results.length} results</span>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
export default CommandPalette;
modules/minimal/ui/command-palette/index.tsx
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| items | { id, label, group, icon?, shortcut?, keywords? }[] | — | Commands. |
| open / onOpenChange | boolean / fn | — | Control (uncontrolled uses the hotkey). |
| onSelect | (item) => void | — | Run handler. |
| hotkey | boolean | true | Toggle with ⌘K / Ctrl+K. |
| placeholder | string | — | Input placeholder. |
| da | string | "minimal" | DA scope for the portal. |
Other modal variants in Minimal
Modal
Radix-based dialog with fade + subtle scale entrance, optional icon, title/description, scrollable body and a tinted footer action bar. Re-applies the DA scope inside the portal.
Popover Card
Anchored popover (Radix Popover) with arrow, optional title + close button and any content — quick forms (snooze), definitions or details. Collision-aware placement.
Sheet
Side sheet (Radix Dialog) sliding in from the right or left over a dimmed overlay: title and description header with close button, scrollable body and footer actions.
Modal in other art directions
Confirm Dialog
Destructive confirmation (Radix AlertDialog) over a hazard-striped overlay: red 'Danger zone' band, consequences list, and a type-the-name field that arms the delete button; async pending state.
Drawer
Side sheet (Radix Dialog) sliding in from the right or left in stepped motion: yellow title band with close square, scrollable body for forms, ruled footer actions.
Modal
Accessible dialog on Radix Dialog: colored title band (primary or danger), close button, scrollable body, footer action bar. Re-applies the DA scope inside the portal. Stepped drop-in entrance.
Onboarding Modal
Multi-step onboarding dialog: a colored visual panel (giant step number by default) next to title, description, segmented progress bar and Back/Next buttons; resets on open.
Bottom Sheet
Mobile-style bottom sheet (Radix Dialog): heavily frosted panel sliding up with a grab handle, title and description; becomes a floating centered card on wider screens.
Dropdown Menu
Frosted dropdown menu (Radix DropdownMenu) built from an items array: labels, icons, shortcuts, separators, checkbox items, nested submenus and a danger item.
Modal
Frosted glass sheet dialog (Radix): 40px backdrop blur, optional icon bubble, rise-and-pop entrance with overshoot, blurred overlay, round close button and footer actions.
Share Dialog
Glass share dialog: invite-by-email field with role select, people-with-access list, a general access switch (invited only / anyone with link) and a copy-link button with confirmation.
Command Dialog
Command palette (Radix Dialog + listbox): a square panel near the top with a large borderless search, grouped results under mono headings, arrow-key navigation with an inverted active row and shortcut hints.
Drawer
Right side panel (Radix Dialog) sliding in over a dimmed page: mono top bar with label and “Close”, large title, scrollable body with ruled rows and a sticky footer.
Lightbox
Full-screen image viewer (Radix Dialog) on ink (stays dark in dark mode): the image frame centered, mono “03 / 04” counter and caption, Prev/Next text buttons and arrow-key navigation, thumbnails strip.
Modal
Square dialog (Radix): a mono label + text “Close” in the top bar, a full-ink rule, a large title, body and right-aligned actions; opens with a quick fade and 8px rise.
Confirm Dialog
Destructive confirmation (Radix AlertDialog): red warning icon, title, consequence text, optional “type VOID to confirm” guard, cancel + red confirm.
Modal
Corporate dialog (Radix): white card with header (title, description, close), divided scrollable body and a tinted footer bar for actions; quick fade + slight scale.
Payment Modal
Pay-invoice dialog: amount summary, card/bank method radio cards, method-specific fields, secure note and a pay button that switches to a success state.
Side Sheet
Detail drawer (Radix Dialog) sliding in from the right: full-height bordered panel with sticky header, scrollable body and footer actions — for record details.
Bottom Sheet
Bottom sheet (Radix Dialog): slides up from the bottom with a grab handle and very rounded top corners; becomes a centered card from sm up.
Confirm Dialog
Gentle confirmation (Radix AlertDialog): a wilting-leaf illustration in a clay circle, serif question, kind consequence text, a sage “keep” action and a quiet clay confirm.
Modal
Soft dialog (Radix): large-radius cream card that rises gently with a slight overshoot, serif title, round close button and pill actions.
Welcome Modal
Onboarding dialog: an illustrated tinted header (sprout growing with each step), serif title and copy, leaf-shaped step dots, Back/Next pills and “Let’s begin” on the last step.
Celebrate Modal
“Week complete!” celebration dialog: pastel confetti shapes burst from the center, trophy emoji, friendly copy, three pastel stat tiles and one pill button.
Confirm Sheet
Destructive confirmation shown as a bottom sheet on mobile and a centered card on desktop: big emoji in a red-tint circle, gentle copy, red confirm with pending state and a soft cancel.
Modal
Soft modal (Radix Dialog): big rounded white card popping in with a small bounce, pastel emoji circle, round close button, body and pill footer actions.
Popover Menu
Rounded dropdown menu (Radix DropdownMenu) with emoji items, keyboard hints, soft highlight, separators and a red danger item; pops in with a bounce.