Command Palette
A real <dialog>, opened with showModal(), the same reasoning as Dialog: a native top-layer stacking context, a native focus trap, and native Escape-to-close, none of it reimplemented in JavaScript. It's controlled, not self-triggering, you decide what opens it, a button, a keyboard shortcut, whatever, and it stays generic: it takes a flat list of items, it doesn't know anything about your app's navigation or commands.
Usage
import { useState } from "react";
import { Button, CommandPalette } from "@kernelui/react";
import type { CommandPaletteItem } from "@kernelui/react";
function App() {
const [open, setOpen] = useState(false);
const items: CommandPaletteItem[] = [
{ id: "new-file", label: "New file", onSelect: () => {} },
{ id: "save", label: "Save", description: "Save the current file", onSelect: () => {} },
];
return (
<>
<Button onClick={() => setOpen(true)}>Open command palette</Button>
<CommandPalette open={open} onOpenChange={setOpen} items={items} />
</>
);
}Props
| Prop | Type | Default |
|---|---|---|
open | boolean | — |
onOpenChange | (open: boolean) => void | — |
items | CommandPaletteItem[] | — |
placeholder | string | "Filter commands" |
emptyMessage | ReactNode | "No results" |
CommandPaletteItem: { id, label, description?, onSelect }.
Accessibility
- Follows the WAI-ARIA combobox pattern: the filter input is a real
role="combobox", focus never leaves it, andaria-activedescendantpoints at whicheverrole="option"is highlighted. - Arrow Up/Down move the active option, clamped at the ends. Enter selects it and closes the palette. Escape closes it too, natively, it isn't handled twice.
- Filtering is a case-insensitive substring match against each item's label, live as you type.
- Clicking the
::backdropcloses the dialog.