Zephora UI

Navigation

Command

Command palette: a filter input driving a keyboard-navigable listbox of grouped items, with empty and loading states and an optional modal dialog wrapper. Supports async sources (useCommandAsync + shouldFilter=false), custom scoring filters, nested pages (useCommandPages + CommandPage) and persistent recent searches (useCommandHistory).

Import

import { Command, CommandInput, CommandList, CommandGroup, CommandItem, CommandEmpty, CommandLoading, CommandDialog, CommandPage, CommandPagesProvider, useCommandAsync, useCommandHistory, useCommandPages } from "@zephora/react";

Examples

Inline palette

Type to filter; ArrowUp/Down move the active item and Enter picks it.

No results.
Go home
Open settings
New file
Delete workspace

Picked: none

Command dialog

CommandDialog hosts the palette in a centered modal overlay.

Async search

useCommandAsync debounces the query and aborts stale requests. The server (here a 600ms fake endpoint) already filtered, so `shouldFilter={false}` keeps every returned item visible; CommandLoading shows while a search is in flight and suppresses CommandEmpty.

Searching…
No project found.

Picked: none

Nested pages

useCommandPages keeps a page stack; CommandPage renders its children only while its name is on top. Picking “Assign to…” pushes the people page, Backspace on an empty query pops back.

No results.
Assign to…
Rename issue

Assigned to: nobody

API

Command props

PropTypeDefaultDescription
shouldFilterbooleantrueSet to false to disable built-in filtering entirely (e.g. when a server performs the search) — every item stays visible in its render order.
filter(value: string, query: string, keywords?: string[]) => numberCustom scoring filter: return 0 to hide an item, any higher number to show it. Matched items are re-ranked by score (highest first) both visually and for keyboard navigation. When omitted, the default case-insensitive substring match is used and item order is preserved.
loopbooleanfalseWraps keyboard navigation from the last item to the first and back.
querystringControlled query value (the CommandInput reads from it).
onQueryChange(query: string) => voidCalled when the query changes.
unstyledbooleanfalseHeadless mode — inherited by all parts.

CommandInput props

PropTypeDefaultDescription
…restOmit<InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "size">Native input props (placeholder, autoFocus…). Value and onChange are managed by the Command root.

CommandItem props

PropTypeDefaultDescription
value *stringUnique value used for filtering, selection and identity.
keywordsstring[]Extra strings matched by the filter.
onSelect(value: string) => voidCalled when the item is picked (click or Enter).
disabledbooleanfalseDisables selection and keyboard navigation.
iconReactNodeLeading icon.
shortcutReactNodeTrailing keyboard shortcut hint.

CommandGroup props

PropTypeDefaultDescription
headingReactNodeOptional heading rendered above the group's items.

CommandDialog props

PropTypeDefaultDescription
openbooleanControlled open state.
defaultOpenbooleanfalseInitial open state when uncontrolled.
onOpenChange(open: boolean) => voidCalled when the open state changes.
aria-labelstring"Command palette"Accessible name of the dialog.

useCommandAsync(options) props

PropTypeDefaultDescription
options.query *stringCurrent query — usually mirrored from the controlled <Command query> state.
options.load *(query: string, signal: AbortSignal) => Promise<T[]>Fetches results for a query. The signal aborts when the query changes, when the hook unmounts, or when a pending debounce is superseded — pass it to fetch() and results from stale requests are dropped.
options.debounceMsnumber200Debounce before calling load.
→ returns{ items: T[]; loading: boolean; error: unknown }Latest results, in-flight flag (render <CommandLoading> while true) and the last load error, if any.

useCommandHistory(key, max?) props

PropTypeDefaultDescription
key *stringStorage namespace — the list persists in localStorage under zephora-cmd-<key>. SSR-safe: degrades to in-memory state when storage is unavailable.
maxnumber5Maximum number of entries kept.
→ returns{ recent: string[]; push: (value: string) => void; clear: () => void }recent is most-recent-first and deduplicated; push records a search (moving an existing entry to the front); clear empties the history and its localStorage entry.

useCommandPages(root?) props

PropTypeDefaultDescription
rootstring"root"Name of the root page the stack starts (and resets) at.
→ returnsCommandPagesState{ page, pages, push, pop, reset }: the active (topmost) page, the full stack (root first), and actions to enter a nested page, leave the current one (no-op at the root) or jump back to the root. Pass it to <CommandPagesProvider value> so <CommandPage name> parts can render only while active.

Keyboard

KeyAction
ArrowDown / ArrowUpMove the active item through matching results.
Home / EndJump to the first / last matching item.
EnterPicks the active item.
EscapeCloses the CommandDialog.