Skip to content
omaris

Navigation

Command Palette

⌘K — a search field over every action in the app, opened from anywhere.

import { CommandPalette } from 'omaris'
Learn

Examples

Basic

Groups of rows, each with a label and optionally an icon, a shortcut, a description or keywords. Type to fuzzy-match — "nwis" finds "New issue". hotkey is ⌘J here because this site already answers to ⌘K.

Last run: nothing yet

Go to
Dashboard
Orders
Settings Control+ ,
Actions
New order Start a blank order Shift+ Control+ O
New issue C
Up arrow Down arrow to navigate Enter to select Escape to close
<script lang="ts">	import { Button, CommandPalette, Kbd, Text, type CommandGroup } from 'omaris';​	let open = $state(false);	let ran = $state('nothing yet');​	const items: CommandGroup[] = [		{			heading: 'Go to',			items: [				{ id: 'dashboard', label: 'Dashboard', icon: home, onselect: () => (ran = 'Dashboard') },				{ id: 'orders', label: 'Orders', icon: box, onselect: () => (ran = 'Orders') },				{					id: 'settings',					label: 'Settings',					keywords: ['preferences', 'account', 'profile'],					icon: gear,					shortcut: 'mod+,',					onselect: () => (ran = 'Settings')				}			]		},		{			heading: 'Actions',			items: [				{					id: 'new-order',					label: 'New order',					description: 'Start a blank order',					icon: plus,					shortcut: 'mod+shift+o',					onselect: () => (ran = 'New order')				},				{					id: 'new-issue',					label: 'New issue',					icon: plus,					shortcut: 'c',					onselect: () => (ran = 'New issue')				}			]		}	];</script>​{#snippet home()}	...{/snippet}{#snippet box()}	...{/snippet}{#snippet gear()}	...{/snippet}{#snippet plus()}	...{/snippet}​<div class="flex flex-col items-center gap-2">	<Button variant="outlined" onclick={() => (open = true)}>		Search commands		<Kbd keys="mod+j" size="sm" variant="flat" />	</Button>	<Text variant="body-sm" tone="muted">Last run: {ran}</Text></div>​<CommandPalette bind:open {items} hotkey="mod+j" />

Pages and recents

A row with children is a page: it opens as its own list, named by a chip in the field; Backspace on an empty field goes back. A query from the top reaches into every page — type "dark". recentKey remembers what was chosen.

Last run: nothing yet

Issue
Assign to…
Change status…
Preferences
Theme…
Up arrow Down arrow to navigate Enter to select Escape to close
<script lang="ts">	import { Button, CommandPalette, Text, type CommandGroup } from 'omaris';​	let open = $state(false);	let ran = $state('nothing yet');​	const TEAM = ['Amina Yusuf', 'Dilan Karim', 'Hevi Salih', 'Omar Nasir'];	const STATUSES = ['Backlog', 'In progress', 'In review', 'Done'];​	const items: CommandGroup[] = [		{			heading: 'Issue',			items: [				{					id: 'assign',					label: 'Assign to…',					placeholder: 'Who should take it?',					children: TEAM.map((name) => ({						id: `assign:${name}`,						label: name,						onselect: () => (ran = `Assigned to ${name}`)					}))				},				{					id: 'status',					label: 'Change status…',					children: STATUSES.map((status) => ({						id: `status:${status}`,						label: status,						onselect: () => (ran = `Moved to ${status}`)					}))				}			]		},		{			heading: 'Preferences',			items: [				{					id: 'theme',					label: 'Theme…',					children: [						{							heading: 'Appearance',							items: [								{ id: 'theme:light', label: 'Light', onselect: () => (ran = 'Light theme') },								{ id: 'theme:dark', label: 'Dark', onselect: () => (ran = 'Dark theme') },								{ id: 'theme:system', label: 'System', onselect: () => (ran = 'System theme') }							]						}					]				}			]		}	];</script>​<div class="flex flex-col items-center gap-2">	<Button variant="tonal" onclick={() => (open = true)}>Issue actions</Button>	<Text variant="body-sm" tone="muted">Last run: {ran}</Text></div>​<CommandPalette	bind:open	{items}	hotkey={false}	recentKey="omaris-docs-palette-recent"	recent={3}	placeholder="What do you want to do?"/>

Overridden

class narrows the dialog, classes reaches the field, the rows and the highlight, footer replaces the key hints — here in Arabic, with every label translated and the page mirrored.

آخر أمر: —

الطلبات
طلب جديد Control+ N
كل الطلبات
المظهر
تغيير المظهر
↑↓ للتنقل · ↵ للاختيار · esc للإغلاق
<script lang="ts">	import { Button, CommandPalette, Text, type CommandGroup } from 'omaris';​	let open = $state(false);	let ran = $state('—');​	const items: CommandGroup[] = [		{			heading: 'الطلبات',			items: [				{ id: 'ar-new', label: 'طلب جديد', shortcut: 'mod+n', onselect: () => (ran = 'طلب جديد') },				{ id: 'ar-orders', label: 'كل الطلبات', onselect: () => (ran = 'كل الطلبات') }			]		},		{			heading: 'المظهر',			items: [				{					id: 'ar-theme',					label: 'تغيير المظهر',					children: [						{ id: 'ar-light', label: 'فاتح', onselect: () => (ran = 'فاتح') },						{ id: 'ar-dark', label: 'داكن', onselect: () => (ran = 'داكن') }					]				}			]		}	];</script>​<div dir="rtl" lang="ar" class="flex flex-col items-center gap-2">	<Button variant="outlined" onclick={() => (open = true)}>لوحة الأوامر</Button>	<Text variant="body-sm" tone="muted">آخر أمر: {ran}</Text>​	<CommandPalette		bind:open		{items}		dir="rtl"		lang="ar"		hotkey={false}		label="لوحة الأوامر"		placeholder="اكتب أمرًا أو ابحث…"		backLabel="رجوع"		clearLabel="مسح"		class="max-w-lg"		classes={{			header: 'bg-surface-container-low',			option: 'rounded-full',			highlight: 'text-tertiary underline decoration-2 underline-offset-4'		}}	>		{#snippet empty(query)}			<Text variant="body-md" tone="muted">لا نتائج لـ «{query}»</Text>		{/snippet}		{#snippet footer()}			<Text as="span" variant="label-sm">↑↓ للتنقل · ↵ للاختيار · esc للإغلاق</Text>		{/snippet}	</CommandPalette></div>

When to use it

Use it for

  • One place to reach everything in an app from the keyboard — pages, actions, records — opened with ⌘K (hotkey, mod+k by default) from anywhere.
  • Actions that take a second choice: "Assign to…", "Move to…", "Theme…" as a row with children, which opens as its own page.
  • Searching records on a server: bind:query to send what is typed, filter={false} to show the answer as it came back, loading meanwhile.
  • The rows people use most, first: recentKey remembers the last recent choices across visits.

Not for

  • Filtering a list that is already on the screen → Search Bar. The palette covers the page; a filter should leave the results in view.
  • Picking a value for a form field → Combobox.
  • The actions of one row or one button → Menu.
  • The app's primary navigation → Navigation Drawer or Navigation Bar. A palette is a shortcut to places, not the only way there — nothing on a phone has ⌘K.

Do

  • Give every row a stable, unique id; it keys the list and it is what recent stores.
  • Add keywords for the words people will actually type — "preferences" and "account" for Settings.
  • Start a label with the verb or the place: "New issue", "Go to billing". The first letters rank highest.
  • Keep a visible way to open it — a button with a Kbd in it, or a search field in the top bar — for touch screens and for people who do not know the shortcut.
  • Put the shortcut on a row only when that shortcut really works outside the palette; the cap is a promise.

Don't

  • Register ⌘K twice. If the app already binds it (a docs search, a browser extension you ship), pass another hotkey or false.
  • Put a form in a page. A page is a list to choose from; a form belongs in a Dialog the chosen row opens.
  • Announce what a row did with a toast when its result is on screen — the page navigated, the theme changed.
  • Nest pages more than two deep. Past that, people lose the thread even with the chips.

API

CommandPalette

⌘K — a search field over every action in the app, opened from anywhere.

Built on Dialog, so it gets the focus trap, the inert page behind and Escape for free, and on a phone it is the same bottom sheet every dialog becomes there — stretched to full height, with the field at the top where the thumb and the keyboard both expect it.

Matching is fuzzy, and ranked the way people type: gs finds "Go to settings", nwis finds "New issue", and the characters that matched are drawn in the accent colour. Results keep their groups, best group first.

An item with children is a page: selecting it opens those as a new list with its name as a chip in the field, Backspace on an empty field goes back, and a search from the top level reaches into every page at once.

The field is a real combobox over a real listbox: ↑ and ↓ move (and wrap), Enter runs, aria-activedescendant keeps a screen reader on the active row, and the active row scrolls itself into view. The global hotkey (mod+k by default) is listened for only while the palette is mounted.

import { CommandPalette } from 'omaris'
<CommandPalette  bind:open  items={[    { heading: 'Go to', items: [{ id: 'inbox', label: 'Inbox', href: '/inbox', shortcut: 'mod+i' }] },    { heading: 'Actions', items: [{ id: 'new', label: 'New issue', shortcut: 'c', onselect: create }] }  ]}/>

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
The Dialog itself.
body
The dialog's body — what the palette's height is measured from.
header
The search row: icon or back button, page chips, field.
searchIcon
The magnifier at the start of the field.
back
The back button, in the magnifier's place on a nested page.
crumbs
The chips naming each page opened on the way here.
crumb
One page chip — a Chip, so this lands on its root.
input
No description in the source yet.
clear
The × that empties the field.
progress
The line under the field while loading.
list
The scrolling listbox.
page
The results of one page, faded in as the page changes.
group
One group of results.
heading
No description in the source yet.
option
No description in the source yet.
icon
The icon well at the start of a row.
text
Label and description, stacked.
label
No description in the source yet.
path
"Theme ›" before a result found inside a page.
description
No description in the source yet.
highlight
The characters that matched the query.
shortcut
The row's shortcut — hidden on touch, where there is no keyboard.
chevron
The › on a row that opens a page.
empty
No description in the source yet.
footer
The key hints along the bottom. Hidden on touch.
hint
No description in the source yet.

Props

open bindable

Defaults to false

boolean

Bindable.

items required
CommandGroup[]

The rows, in groups.

query bindable

Defaults to ''

string

What is typed in the field. Bindable, for searching a server as it changes.

placeholder

Defaults to 'Type a command or search…'

string
hotkey

Defaults to 'mod+k'

string | string[] | false

The shortcut that toggles it from anywhere on the page — 'mod+k' is ⌘K on a Mac and Ctrl+K elsewhere. false for none. Listened for only while the palette is mounted.

recent

Defaults to 5

number

How many recently chosen rows to remember and show first. Needs recentKey.

recentKey
string

localStorage key the recent rows are kept under. Without one, nothing is remembered.

recentHeading

Defaults to 'Recent'

string

Heading over the recent rows.

loading

Defaults to false

boolean

Draw a progress line under the field — for results that are on their way.

empty
Snippet<[string]>

Drawn when nothing matches. Handed the query.

filter
CommandFilter | false

Replace the fuzzy ranking. false shows items exactly as given, for a list a server has already filtered.

limit

Defaults to 100

number

Most results shown for a query. The best ones, by score.

onselect
(item: CommandItem) => void

Called with every row chosen, after the row's own onselect.

closeOnSelect

Defaults to true

boolean

Close once a row has been chosen. On by default.

label

Defaults to 'Command palette'

string

Accessible name of the dialog.

backLabel

Defaults to 'Back'

string

Accessible name of the back button on a nested page.

clearLabel

Defaults to 'Clear'

string

Accessible name of the button that empties the field.

hints

Defaults to true

boolean

The key hints along the bottom. Hidden on touch either way.

footer
Snippet

Replaces the key hints — in another language, say. Hidden on touch too.

mobileQuery

Defaults to '(max-width: 639px)'

string

What counts as a phone — below it the palette is a full-height sheet.

onclose
() => void

Called after it has finished closing.

class
string
classes
CommandPaletteClasses

Per-part Tailwind overrides. class still covers the dialog.