Components · Overlays
Command menu
A command palette: filter as you type, arrows and Enter, grouped commands, ⌘K or Ctrl+K to open.
9.7% of the 2,062 sites in The UI Index have a search (200 sites).
Both previews render the same markup; the HTML tab below is that markup without a framework.
Install
- React
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ command-menu.json - Astro
- npx shadcn@
latest add https:// www.themes.dev/ caseroom/ r/ astro/ command-menu.json
Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS and the script.
// Command menu behaviour, shared by the Astro, React and HTML versions (WAI-ARIA combobox with a listbox popup).
// The dialog itself (opening, focus, backdrop, focus return) is dialog.client's. Here: filtering as you type (label
// and keywords, case-insensitive; empty groups hide; "No results" is announced), aria-activedescendant, Up/Down
// (wrapping) and Enter, Escape that clears a non-empty field before it closes, a click on an option, and ⌘K /
// Ctrl+K to open the page's first [data-command] menu. Running a command dispatches a bubbling `command-select`
// event ({ value, label }) from the dialog and closes it.
import { openDialog } from '../dialog/dialog.client';
declare global {
interface Window {
__caseroomCommand?: boolean;
}
}
const parts = (d: HTMLElement) => ({
input: d.querySelector<HTMLInputElement>('[role=combobox]')!,
options: [...d.querySelectorAll<HTMLElement>('[role=option]')],
visible: () => [...d.querySelectorAll<HTMLElement>('[role=option]:not([hidden])')],
empty: d.querySelector<HTMLElement>('[data-command-empty]'),
});
function highlight(d: HTMLElement, option: HTMLElement | undefined) {
const { input, options } = parts(d);
for (const o of options) o.setAttribute('aria-selected', String(o === option));
if (option) {
input.setAttribute('aria-activedescendant', option.id);
option.scrollIntoView({ block: 'nearest' });
} else input.removeAttribute('aria-activedescendant');
}
function filter(d: HTMLElement) {
const { input, options, visible, empty } = parts(d);
const q = input.value.trim().toLowerCase();
for (const o of options) o.hidden = !!q && !`${o.textContent} ${o.dataset.keywords ?? ''}`.toLowerCase().includes(q);
for (const g of d.querySelectorAll<HTMLElement>('[role=group]')) g.hidden = !g.querySelector('[role=option]:not([hidden])');
const shown = visible();
if (empty) empty.textContent = shown.length ? '' : `No results for “${input.value.trim()}”`;
highlight(d, shown[0]);
}
function run(d: HTMLDialogElement, option: HTMLElement) {
d.dispatchEvent(new CustomEvent('command-select', { bubbles: true, detail: { value: option.dataset.value, label: option.textContent?.trim() } }));
d.close();
}
function reset(d: HTMLElement) {
const { input } = parts(d);
input.value = '';
filter(d);
}
if (typeof window !== 'undefined' && !window.__caseroomCommand) {
window.__caseroomCommand = true;
document.addEventListener('input', (e) => {
const d = (e.target as Element).closest?.<HTMLDialogElement>('dialog[data-command]');
if (d && (e.target as Element).matches('[role=combobox]')) filter(d);
});
document.addEventListener(
'keydown',
(e) => {
// ⌘K / Ctrl+K anywhere opens the menu (the first one on the page).
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
const d = document.querySelector<HTMLDialogElement>('dialog[data-command]');
if (d && !d.open) {
e.preventDefault();
reset(d);
openDialog(d, document.activeElement as HTMLElement);
}
return;
}
const d = (e.target as Element).closest?.<HTMLDialogElement>('dialog[data-command]');
if (!d || !(e.target as Element).matches('[role=combobox]')) return;
const shown = parts(d).visible();
const current = shown.findIndex((o) => o.getAttribute('aria-selected') === 'true');
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (!shown.length) return;
const next = e.key === 'ArrowDown' ? (current + 1) % shown.length : (current - 1 + shown.length) % shown.length;
highlight(d, shown[next]);
} else if (e.key === 'Enter') {
e.preventDefault();
if (shown[current]) run(d, shown[current]);
} else if (e.key === 'Escape' && parts(d).input.value) {
// First Escape clears the search; the next one closes (the dialog's own Escape).
e.preventDefault();
reset(d);
}
},
true,
);
document.addEventListener('click', (e) => {
const option = (e.target as Element).closest<HTMLElement>('dialog[data-command] [role=option]');
if (option) run(option.closest('dialog')!, option);
});
document.addEventListener('pointermove', (e) => {
const option = (e.target as Element).closest?.<HTMLElement>('dialog[data-command] [role=option]');
if (option && option.getAttribute('aria-selected') !== 'true') highlight(option.closest('dialog')!, option);
});
// Every time it opens, it starts empty with the first command highlighted.
document.addEventListener(
'toggle',
(e) => {
const d = e.target as HTMLElement;
if (d instanceof HTMLDialogElement && d.matches('[data-command]') && d.open) reset(d);
},
true,
);
}
export {};import { useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';
import './command-menu.client';
import { command, SEARCH_PATH, type CommandGroup } from './command-menu.variants';
export type { CommandGroup };
export interface CommandMenuProps {
/** Unique on the page. A trigger opens it with command="show-modal" commandfor={id}; ⌘K / Ctrl+K opens it too. */
id: string;
/** The accessible name of the dialog and the list, e.g. "Commands". */
label: string;
groups: CommandGroup[];
placeholder?: string;
/** Called with the command that was run. */
onSelect?: (detail: { value: string; label: string }) => void;
className?: string;
}
/** A command palette: a modal with a filtering combobox and a grouped listbox (WAI-ARIA), ⌘K / Ctrl+K to open. */
export function CommandMenu({ id, label, groups, placeholder = 'Type a command or search…', onSelect, className }: CommandMenuProps) {
const ref = useRef<HTMLDialogElement>(null);
useEffect(() => {
const d = ref.current;
if (!d || !onSelect) return;
const on = (e: Event) => onSelect((e as CustomEvent).detail);
d.addEventListener('command-select', on);
return () => d.removeEventListener('command-select', on);
}, [onSelect]);
return (
<dialog ref={ref} id={id} data-dialog="" data-command="" aria-label={label} className={cn(command.root, className)}>
<div data-dialog-body="">
<div className={command.search}>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true" className={command.searchIcon}><path d={SEARCH_PATH} /></svg>
<input type="text" role="combobox" aria-expanded="true" aria-controls={`${id}-list`} aria-autocomplete="list" aria-label={label} autoComplete="off" spellCheck="false" placeholder={placeholder} className={command.input} />
</div>
<div id={`${id}-list`} role="listbox" aria-label={label} className={command.list}>
{groups.map((g, gi) => (
<div key={g.heading} role="group" aria-labelledby={`${id}-g${gi}`} className={command.group}>
<div id={`${id}-g${gi}`} role="presentation" className={command.heading}>{g.heading}</div>
{g.items.map((it) => (
<div key={it.value} role="option" id={`${id}-${it.value}`} aria-selected="false" data-value={it.value} data-keywords={it.keywords} className={command.option}>
{it.label}
{it.shortcut && <kbd className={command.shortcut}>{it.shortcut}</kbd>}
</div>
))}
</div>
))}
</div>
<p data-command-empty="" role="status" className={command.empty}></p>
</div>
<div className={command.footer} aria-hidden="true">
<span><kbd className={command.key}>↑</kbd> <kbd className={command.key}>↓</kbd> to move</span>
<span><kbd className={command.key}>↵</kbd> to run</span>
<span><kbd className={command.key}>esc</kbd> to close</span>
</div>
</dialog>
);
}import { dialog } from '../dialog/dialog.variants';
/** A command menu: a modal dialog near the top with a search field (combobox) and a list of commands (listbox),
* grouped. Typing filters, arrows move the highlight, Enter runs, Escape clears the field and then closes. Opened
* by its trigger or by ⌘K / Ctrl+K. */
export const command = {
root: `${dialog.root} mt-[12vh] mb-auto max-w-lg`,
search: 'flex items-center gap-2 border-b px-4',
searchIcon: 'size-4 shrink-0 text-muted-foreground',
input: 'h-12 w-full min-w-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground',
list: 'max-h-80 overflow-y-auto p-2',
group: 'py-1',
heading: 'px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground',
option:
'flex h-10 cursor-pointer items-center gap-3 rounded-md px-2 text-sm select-none aria-selected:bg-accent active:bg-pressed [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground',
shortcut: 'ml-auto font-sans text-[13px] text-muted-foreground',
// Always present (a live region must exist before it speaks); takes no room while empty.
empty: 'px-4 py-8 text-center text-sm text-muted-foreground empty:p-0',
footer: 'flex flex-wrap items-center gap-x-4 gap-y-1 border-t bg-muted/50 px-4 py-2.5 text-xs text-muted-foreground',
key: 'inline-flex h-5 min-w-5 items-center justify-center rounded border bg-background px-1 font-sans text-[11px] text-foreground',
};
export type CommandItem = { value: string; label: string; shortcut?: string; keywords?: string };
export type CommandGroup = { heading: string; items: CommandItem[] };
export const SEARCH_PATH = 'M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5';---
import { cn } from '@/lib/utils';
import { command, SEARCH_PATH, type CommandGroup } from './command-menu.variants';
interface Props {
/** Unique on the page. A trigger opens it with command="show-modal" commandfor={id}; ⌘K / Ctrl+K opens it too. */
id: string;
/** The accessible name of the dialog and the list, e.g. "Commands". */
label: string;
groups: CommandGroup[];
placeholder?: string;
class?: string;
}
const { id, label, groups, placeholder = 'Type a command or search…', class: className } = Astro.props;
---
<dialog id={id} data-dialog data-command aria-label={label} class={cn(command.root, className)}>
<div data-dialog-body>
<div class={command.search}>
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class={command.searchIcon}><path d={SEARCH_PATH} /></svg>
<input type="text" role="combobox" aria-expanded="true" aria-controls={`${id}-list`} aria-autocomplete="list" aria-label={label} autocomplete="off" spellcheck="false" placeholder={placeholder} class={command.input} />
</div>
<div id={`${id}-list`} role="listbox" aria-label={label} class={command.list}>
{
groups.map((g, gi) => (
<div role="group" aria-labelledby={`${id}-g${gi}`} class={command.group}>
<div id={`${id}-g${gi}`} role="presentation" class={command.heading}>{g.heading}</div>
{g.items.map((it) => (
<div role="option" id={`${id}-${it.value}`} aria-selected="false" data-value={it.value} data-keywords={it.keywords} class={command.option}>
{it.label}
{it.shortcut && <kbd class={command.shortcut}>{it.shortcut}</kbd>}
</div>
))}
</div>
))
}
</div>
<p data-command-empty role="status" class={command.empty}></p>
</div>
<div class={command.footer} aria-hidden="true">
<span><kbd class={command.key}>↑</kbd> <kbd class={command.key}>↓</kbd> to move</span>
<span><kbd class={command.key}>↵</kbd> to run</span>
<span><kbd class={command.key}>esc</kbd> to close</span>
</div>
</dialog>
<script>
import './command-menu.client';
</script>// Command menu behaviour, shared by the Astro, React and HTML versions (WAI-ARIA combobox with a listbox popup).
// The dialog itself (opening, focus, backdrop, focus return) is dialog.client's. Here: filtering as you type (label
// and keywords, case-insensitive; empty groups hide; "No results" is announced), aria-activedescendant, Up/Down
// (wrapping) and Enter, Escape that clears a non-empty field before it closes, a click on an option, and ⌘K /
// Ctrl+K to open the page's first [data-command] menu. Running a command dispatches a bubbling `command-select`
// event ({ value, label }) from the dialog and closes it.
import { openDialog } from '../dialog/dialog.client';
declare global {
interface Window {
__caseroomCommand?: boolean;
}
}
const parts = (d: HTMLElement) => ({
input: d.querySelector<HTMLInputElement>('[role=combobox]')!,
options: [...d.querySelectorAll<HTMLElement>('[role=option]')],
visible: () => [...d.querySelectorAll<HTMLElement>('[role=option]:not([hidden])')],
empty: d.querySelector<HTMLElement>('[data-command-empty]'),
});
function highlight(d: HTMLElement, option: HTMLElement | undefined) {
const { input, options } = parts(d);
for (const o of options) o.setAttribute('aria-selected', String(o === option));
if (option) {
input.setAttribute('aria-activedescendant', option.id);
option.scrollIntoView({ block: 'nearest' });
} else input.removeAttribute('aria-activedescendant');
}
function filter(d: HTMLElement) {
const { input, options, visible, empty } = parts(d);
const q = input.value.trim().toLowerCase();
for (const o of options) o.hidden = !!q && !`${o.textContent} ${o.dataset.keywords ?? ''}`.toLowerCase().includes(q);
for (const g of d.querySelectorAll<HTMLElement>('[role=group]')) g.hidden = !g.querySelector('[role=option]:not([hidden])');
const shown = visible();
if (empty) empty.textContent = shown.length ? '' : `No results for “${input.value.trim()}”`;
highlight(d, shown[0]);
}
function run(d: HTMLDialogElement, option: HTMLElement) {
d.dispatchEvent(new CustomEvent('command-select', { bubbles: true, detail: { value: option.dataset.value, label: option.textContent?.trim() } }));
d.close();
}
function reset(d: HTMLElement) {
const { input } = parts(d);
input.value = '';
filter(d);
}
if (typeof window !== 'undefined' && !window.__caseroomCommand) {
window.__caseroomCommand = true;
document.addEventListener('input', (e) => {
const d = (e.target as Element).closest?.<HTMLDialogElement>('dialog[data-command]');
if (d && (e.target as Element).matches('[role=combobox]')) filter(d);
});
document.addEventListener(
'keydown',
(e) => {
// ⌘K / Ctrl+K anywhere opens the menu (the first one on the page).
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
const d = document.querySelector<HTMLDialogElement>('dialog[data-command]');
if (d && !d.open) {
e.preventDefault();
reset(d);
openDialog(d, document.activeElement as HTMLElement);
}
return;
}
const d = (e.target as Element).closest?.<HTMLDialogElement>('dialog[data-command]');
if (!d || !(e.target as Element).matches('[role=combobox]')) return;
const shown = parts(d).visible();
const current = shown.findIndex((o) => o.getAttribute('aria-selected') === 'true');
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
if (!shown.length) return;
const next = e.key === 'ArrowDown' ? (current + 1) % shown.length : (current - 1 + shown.length) % shown.length;
highlight(d, shown[next]);
} else if (e.key === 'Enter') {
e.preventDefault();
if (shown[current]) run(d, shown[current]);
} else if (e.key === 'Escape' && parts(d).input.value) {
// First Escape clears the search; the next one closes (the dialog's own Escape).
e.preventDefault();
reset(d);
}
},
true,
);
document.addEventListener('click', (e) => {
const option = (e.target as Element).closest<HTMLElement>('dialog[data-command] [role=option]');
if (option) run(option.closest('dialog')!, option);
});
document.addEventListener('pointermove', (e) => {
const option = (e.target as Element).closest?.<HTMLElement>('dialog[data-command] [role=option]');
if (option && option.getAttribute('aria-selected') !== 'true') highlight(option.closest('dialog')!, option);
});
// Every time it opens, it starts empty with the first command highlighted.
document.addEventListener(
'toggle',
(e) => {
const d = e.target as HTMLElement;
if (d instanceof HTMLDialogElement && d.matches('[data-command]') && d.open) reset(d);
},
true,
);
}
export {};import { dialog } from '../dialog/dialog.variants';
/** A command menu: a modal dialog near the top with a search field (combobox) and a list of commands (listbox),
* grouped. Typing filters, arrows move the highlight, Enter runs, Escape clears the field and then closes. Opened
* by its trigger or by ⌘K / Ctrl+K. */
export const command = {
root: `${dialog.root} mt-[12vh] mb-auto max-w-lg`,
search: 'flex items-center gap-2 border-b px-4',
searchIcon: 'size-4 shrink-0 text-muted-foreground',
input: 'h-12 w-full min-w-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground',
list: 'max-h-80 overflow-y-auto p-2',
group: 'py-1',
heading: 'px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground',
option:
'flex h-10 cursor-pointer items-center gap-3 rounded-md px-2 text-sm select-none aria-selected:bg-accent active:bg-pressed [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground',
shortcut: 'ml-auto font-sans text-[13px] text-muted-foreground',
// Always present (a live region must exist before it speaks); takes no room while empty.
empty: 'px-4 py-8 text-center text-sm text-muted-foreground empty:p-0',
footer: 'flex flex-wrap items-center gap-x-4 gap-y-1 border-t bg-muted/50 px-4 py-2.5 text-xs text-muted-foreground',
key: 'inline-flex h-5 min-w-5 items-center justify-center rounded border bg-background px-1 font-sans text-[11px] text-foreground',
};
export type CommandItem = { value: string; label: string; shortcut?: string; keywords?: string };
export type CommandGroup = { heading: string; items: CommandItem[] };
export const SEARCH_PATH = 'M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5';<div class="flex flex-col items-center gap-3">
<button type="button" class="inline-flex shrink-0 cursor-pointer items-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,background-color,border-color,transform] select-none active:scale-[0.97] aria-busy:cursor-progress disabled:pointer-events-none disabled:opacity-50 aria-busy:disabled:opacity-80 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border border-input bg-background hover:bg-accent hover:text-accent-foreground active:bg-pressed h-9 px-4 w-64 justify-between text-muted-foreground" command="show-modal" commandfor="ex-command">
Search commands…
<span class="flex gap-1">
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded-[4px] border border-b-2 bg-muted px-1 font-mono text-[11px] font-medium text-muted-foreground">
⌘
</kbd>
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded-[4px] border border-b-2 bg-muted px-1 font-mono text-[11px] font-medium text-muted-foreground">
K
</kbd>
</span>
</button>
</div>
<dialog id="ex-command" data-dialog data-command aria-label="Commands" class="m-auto w-[calc(100%-2rem)] rounded-xl border bg-popover p-0 text-popover-foreground shadow-2xl dark:shadow-[0_24px_64px_rgb(0_0_0/0.7)] backdrop:bg-black/50 backdrop:backdrop-blur-[2px] opacity-0 translate-y-1 transition-[opacity,translate,display,overlay] transition-discrete duration-150 ease-out open:opacity-100 open:translate-y-0 starting:open:opacity-0 starting:open:translate-y-1 mt-[12vh] mb-auto max-w-lg">
<div data-dialog-body>
<div class="flex items-center gap-2 border-b px-4">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true" class="size-4 shrink-0 text-muted-foreground"><path d="M7 12.5a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM11 11l3.5 3.5"></path></svg>
<input type="text" role="combobox" aria-expanded="true" aria-controls="ex-command-list" aria-autocomplete="list" aria-label="Commands" autocomplete="off" spellcheck="false" placeholder="Type a command or search…" class="h-12 w-full min-w-0 bg-transparent text-sm outline-none placeholder:text-muted-foreground">
</div>
<div id="ex-command-list" role="listbox" aria-label="Commands" class="max-h-80 overflow-y-auto p-2">
<div role="group" aria-labelledby="ex-command-g0" class="py-1">
<div id="ex-command-g0" role="presentation" class="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground">Pages</div>
<div role="option" id="ex-command-dashboard" aria-selected="false" data-value="dashboard" class="flex h-10 cursor-pointer items-center gap-3 rounded-md px-2 text-sm select-none aria-selected:bg-accent active:bg-pressed [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">
Dashboard
<kbd class="ml-auto font-sans text-[13px] text-muted-foreground">G D</kbd>
</div>
<div role="option" id="ex-command-deploys" aria-selected="false" data-value="deploys" class="flex h-10 cursor-pointer items-center gap-3 rounded-md px-2 text-sm select-none aria-selected:bg-accent active:bg-pressed [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">
Deploys
<kbd class="ml-auto font-sans text-[13px] text-muted-foreground">G P</kbd>
</div>
<div role="option" id="ex-command-settings" aria-selected="false" data-value="settings" data-keywords="preferences account" class="flex h-10 cursor-pointer items-center gap-3 rounded-md px-2 text-sm select-none aria-selected:bg-accent active:bg-pressed [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">
Settings
<kbd class="ml-auto font-sans text-[13px] text-muted-foreground">G S</kbd>
</div>
</div>
<div role="group" aria-labelledby="ex-command-g1" class="py-1">
<div id="ex-command-g1" role="presentation" class="px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground">Actions</div>
<div role="option" id="ex-command-new-project" aria-selected="false" data-value="new-project" data-keywords="create add" class="flex h-10 cursor-pointer items-center gap-3 rounded-md px-2 text-sm select-none aria-selected:bg-accent active:bg-pressed [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">
New project
</div>
<div role="option" id="ex-command-invite" aria-selected="false" data-value="invite" data-keywords="member user" class="flex h-10 cursor-pointer items-center gap-3 rounded-md px-2 text-sm select-none aria-selected:bg-accent active:bg-pressed [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">
Invite a teammate
</div>
<div role="option" id="ex-command-theme" aria-selected="false" data-value="theme" data-keywords="appearance light theme" class="flex h-10 cursor-pointer items-center gap-3 rounded-md px-2 text-sm select-none aria-selected:bg-accent active:bg-pressed [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">
Toggle dark mode
</div>
</div>
</div>
<p data-command-empty role="status" class="px-4 py-8 text-center text-sm text-muted-foreground empty:p-0"></p>
</div>
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 border-t bg-muted/50 px-4 py-2.5 text-xs text-muted-foreground" aria-hidden="true">
<span>
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border bg-background px-1 font-sans text-[11px] text-foreground">↑</kbd>
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border bg-background px-1 font-sans text-[11px] text-foreground">↓</kbd>
to move
</span>
<span>
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border bg-background px-1 font-sans text-[11px] text-foreground">↵</kbd>
to run
</span>
<span>
<kbd class="inline-flex h-5 min-w-5 items-center justify-center rounded border bg-background px-1 font-sans text-[11px] text-foreground">esc</kbd>
to close
</span>
</div>
</dialog><script type="module" src="/caseroom/r/html/command-menu.js"></script>