Skip to content
TopClass UI

Command palette

A searchable palette of actions, opened with Ctrl+K or Cmd+K. It loads on its first open, never with the page.

client - size not measured yet, lazy

Command palette

client - size not measured yet, lazy

A searchable palette of actions, opened with Ctrl+K or Cmd+K. It loads on its first open, never with the page.

Install

Try it

visibleRows
loadMoreThreshold
defaultOpen

The code

command.tsx
'use client'

import { useState } from 'react'

import { magnifyingGlass } from 'topclassui/icons/magnifying-glass'
import { Button } from 'topclassui/button'
import { Svg } from 'topclassui/svg'
import { Command } from 'topclassui/command'
import type { CommandDesignProps } from 'topclassui/command'

interface Entry { id: string, label: string }

const COMMANDS: Entry[] = [
  { id: 'button', label: 'Button' },
  { id: 'dialog', label: 'Dialog' },
  { id: 'drawer', label: 'Drawer' },
  { id: 'popover', label: 'Popover' },
  { id: 'tabs', label: 'Tabs' },
  { id: 'accordion', label: 'Accordion' },
  { id: 'table', label: 'Table' },
  { id: 'pagination', label: 'Pagination' },
  { id: 'breadcrumb', label: 'Breadcrumb' },
  { id: 'tooltip', label: 'Tooltip' },
]

/** The docs stage: the basic palette, with the schema's props passed through, so the Try it
 * controls drive it. defaultOpen is where the open state starts; the Button opens it too. */
export default function Demo({ visibleRows, virtualize, shortcut, defaultOpen }: CommandDesignProps = {}) {
  const [open, setOpen] = useState(defaultOpen ?? false)
  return (
    <div className="grid justify-items-start gap-3">
      <Button variant="outline" onClick={() => { setOpen(true) }}><Svg icon={magnifyingGlass} />Search</Button>
      <Command
        items={COMMANDS}
        getKey={(command) => command.id}
        getLabel={(command) => command.label}
        onSelect={() => undefined}
        visibleRows={visibleRows}
        virtualize={virtualize}
        shortcut={shortcut}
        open={open}
        onOpenChange={setOpen}
      />
    </div>
  )
}

getKey, getLabel and onSelect are functions, so the palette is configured inside a client file.

Every state

6 states

Import from the package

topclassui/command
import { Command } from 'topclassui/command'

Install once:npm i topclassui@beta zod

Examples

  • Command is the whole palette; its parts are not exported.
  • Open it with open and onOpenChange from your button, or leave both out and let shortcut open it.
  • Before its first open it renders nothing and has loaded no Base UI; it stays mounted after that.
  • Every word of the query matches anywhere in getLabel, in any case, and the order of items is kept.
  • getGroup returns a heading, and renderItem draws a row that stays one line tall.
  • virtualize is 'auto' (windowing above 200 rows), true or false; visibleRows sets how many rows show.
basic

Nothing ran yet.

groups

Nothing ran yet.

with-shortcuts
empty-state
loading
thousand

Nothing ran yet.

ten-thousand

Nothing ran yet.

long-content
long-page

Section 1. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 2. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 3. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 4. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 5. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 6. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 7. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 8. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 9. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 10. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 11. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 12. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 13. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 14. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 15. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 16. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 17. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 18. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 19. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 20. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 21. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 22. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 23. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 24. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 25. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 26. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 27. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 28. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 29. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 30. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 31. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 32. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 33. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 34. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 35. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 36. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 37. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 38. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 39. The page goes on below the palette, so a scroll that escaped it would move this text.

Section 40. The page goes on below the palette, so a scroll that escaped it would move this text.

with-tooltip
In Arabic, right to left

Props

The props of Command
PropTypeDefaultLimitsDescription
getKey*(item: T) => string--Stable identity across pages, so a row keeps its place when a page is prepended. Client-only.
getLabel*(item: T) => string--The text a row shows and the query is matched against, word by word. Client-only, as is every function here.
items*readonly T[]--
onSelect*(item: T) => void--Called with the chosen item, by a press or by Enter; the palette has closed by then. Client-only.
classNamestring--Merged last onto the popup, so a consumer class wins a conflict with its own.
defaultOpenbooleanfalse-Whether it starts open, when open is not controlled.
emptyFallbackReactNode--
getGroup((item: T) => string | undefined)--The heading a row sits under; rows with none come first, in a group with no heading. Client-only.
hasMoreboolean--
loadingFallbackReactNode--
loadMore(() => void | Promise<void>)--Client-only, or a Server Action. Called once when the window nears the end.
loadMoreThresholdinteger, min 0, max 20010min 0, max 200Rows from the end at which loadMore is called, clamped to 0 to 200.
onOpenChange((open: boolean) => void)--Called with the open state the palette asks for. Client-only.
onQueryChange((query: string) => void)--Called with each new query, so the caller can fetch its first page from the server. Client-only, or a Server Action.
openboolean--Whether it is open. Use with onOpenChange when controlled.
renderItem((item: T) => ReactNode)--What a row holds in place of its label. It stays one row tall. Client-only.
shortcutunknown"k"-The key that opens it with Ctrl or Cmd, wherever focus is; null for none.
statusExclude<Status, 'error'>--Never 'error': with no errorFallback, a failed fetch would read as No results.
virtualizeunknown"auto"-Windowing: auto mounts only the rows in view above the provider threshold (200).
visibleRowsinteger, min 1, max 1007min 1, max 100Rows the list shows before it scrolls, clamped to 1 to 100; left out, the --tc-list-rows knob.

Keyboard and accessibility

The keyboard of Command
KeysWhat it does
Ctrl+KCmd+KOpens the palette from anywhere, with focus in the input. shortcut changes the key; null removes it.
ArrowDownArrowUpMove the active row. The first match is active as you type.
EnterCloses the palette, then runs the active row once.
EscapeCloses a tooltip open inside first, then the palette. Focus returns to what had it before.
  • The input is a combobox over a listbox, with aria-activedescendant on the active row.
  • With no match the list is removed and the empty text shows, so no empty listbox is left to land in.
  • Home and End stay with the input, for its caret.

Right to left

The glass sits at the right of the input and the rows align right. Ctrl+K also works under an Arabic keyboard layout, because the key is matched by its place too.

Server and client

A client component: it renders on the server and hydrates in the browser.

Files: index.tsx, client.tsx.

On the chip: client - size not measured yet, lazy.