Navigation
Command Palette
⌘K — a search field over every action in the app, opened from anywhere.
import { CommandPalette } from 'omaris' 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
<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
<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?"/> Server search
When a server does the searching: bind:query to send what is typed, filter={false} to show the answer exactly as it came back, and loading for the line under the field while it is on its way.
Picked: nobody yet
<script lang="ts"> import { Button, CommandPalette, Text, type CommandGroup } from 'omaris'; const CUSTOMERS = [ 'Zagros Café', 'Nishtiman Books', 'Erbil Bakery', 'Tigris Tailors', 'Sulaymaniyah Print House', 'Duhok Dairy', 'Basra Marine Supply' ]; let open = $state(false); let query = $state(''); let loading = $state(false); let results = $state<CommandGroup[]>([]); let picked = $state('nobody yet'); /** Stands in for `fetch('/api/customers?q=…')`. */ $effect(() => { const q = query.trim().toLowerCase(); loading = true; const timer = setTimeout(() => { const items = CUSTOMERS.filter((name) => name.toLowerCase().includes(q)).map((name) => ({ id: name, label: name, description: 'Customer', onselect: () => (picked = name) })); results = items.length ? [{ heading: 'Customers', items }] : []; loading = false; }, 400); return () => clearTimeout(timer); });</script><div class="flex flex-col items-center gap-2"> <Button variant="outlined" onclick={() => (open = true)}>Find a customer</Button> <Text variant="body-sm" tone="muted">Picked: {picked}</Text></div><CommandPalette bind:open bind:query items={results} filter={false} {loading} hotkey={false} placeholder="Search customers…"/> 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.
آخر أمر: —
<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+kby 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:queryto send what is typed,filter={false}to show the answer as it came back,loadingmeanwhile. - The rows people use most, first:
recentKeyremembers the lastrecentchoices 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 whatrecentstores. - Add
keywordsfor 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
Kbdin it, or a search field in the top bar — for touch screens and for people who do not know the shortcut. - Put the
shortcuton 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
hotkeyorfalse. - 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
Dialogitself. 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 bindableDefaults to false
boolean Bindable.
items required CommandGroup[] The rows, in groups.
query bindableDefaults 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.