Skip to main content

CommandPalette

Search grouped commands and navigation targets from an inline surface.

Use CommandPalette for searchable commands or navigation targets. The application decides when to open it and registers any global shortcut.

Basic usage#

import { CommandPalette } from 'moraine'

export function Example() {
  return (
    <CommandPalette
      groups={[{ id: 'pages', label: 'Pages', items: [{ value: 'settings', label: 'Settings' }] }]}
    />
  )
}

Playground#

Workspace Actions
Create New Issue⌘+N
Open Notifications⌘+I
Sync RoadmapPull the latest project updates
Navigation
Dashboard Overview
Projects Directory
Settings & Security
↑+↓Navigate
↵Select
EscClose
Props
Slots

Anatomy#

CommandPalette [component; slot=root]
├── inputWrapper [slot]
│   ├── inputLeading [slot]
│   ├── input [slot]
│   └── close [slot]
├── listbox [slot]
│   ├── group [slot]
│   │   ├── groupLabel [slot]
│   │   └── item [slot]
│   │       ├── itemLeading [slot]
│   │       ├── itemWrapper [slot]
│   │       │   ├── itemLabel [slot]
│   │       │   └── itemDescription [slot]
│   │       └── itemTrailing [slot]
│   └── empty [slot]
└── footer [slot]

Group labels and items may be flattened into sibling rows when virtualized. Trailing descriptions render inside itemLabel.

Usage#

Groups, filtering, and selection#

Pass groups and items from application state. The palette filters the supplied collection and invokes item selection callbacks; fetching commands, routing, and global keyboard shortcuts are application-owned.

import { CommandPalette, Icon } from 'moraine'
import type { CommandPaletteT } from 'moraine'
import { createSignal } from 'solid-js'

const COMMAND_GROUPS: CommandPaletteT.Group[] = [
  {
    id: 'workspace',
    label: 'Workspace sections',
    items: [
      {
        value: 'overview',
        label: 'Overview',
        description: 'Recent activity and open work',
        leadingRender: () => <Icon name="i-lucide:layout-dashboard" />,
      },
      {
        value: 'members',
        label: 'Members',
        description: 'People with workspace access',
        leadingRender: () => <Icon name="i-lucide:users" />,
      },
      {
        value: 'security',
        label: 'Security',
        description: 'Sign-in and access policies',
        leadingRender: () => <Icon name="i-lucide:shield-check" />,
      },
    ],
  },
]

export function GroupsFiltering() {
  const [section, setSection] = createSignal('overview')

  return (
    <div class="max-w-md w-full space-y-3">
      <CommandPalette
        autofocus={false}
        placeholder="Find a workspace section..."
        groups={COMMAND_GROUPS}
        onSelect={(item) => setSection(item.value)}
      />
      <p class="text-sm">Current section: {section()}</p>
    </div>
  )
}

Custom states and navigation#

Use the custom item renderer for a changed item layout, and provide loading or empty content when those states need explanation. Sub-navigation should represent a short, deliberate branch of commands rather than a complete application shell.

import { CommandPalette } from 'moraine'

export function CustomStates() {
  return (
    <div class="b-1 b-border rounded-xl max-w-md w-full shadow-lg overflow-hidden">
      <CommandPalette
        autofocus={false}
        placeholder="Search empty query..."
        groups={[]}
        emptyRender={() => (
          <p class="text-xs text-muted-foreground p-6 text-center">
            No commands found matching your query.
          </p>
        )}
      />
    </div>
  )
}

Keyboard interaction#

Key Description
↓ Moves to the next available command.
↑ Moves to the previous available command.
↵ Activates the current command.
Esc Clears the current query or requests close according to current state.
↖ / ↘ Moves to the first or last available command.

Examples#

Custom item rendering#

import { Badge, CommandPalette, Icon } from 'moraine'
import type { CommandPaletteT } from 'moraine'

interface TeamCommand extends CommandPaletteT.Item {
  owner: string
  status: 'ready' | 'blocked'
}

const GROUPS: CommandPaletteT.Group<TeamCommand>[] = [
  {
    id: 'projects',
    label: 'Projects',
    items: [
      {
        value: 'moraine-docs',
        label: 'Moraine Docs',
        description: 'Update examples for the next release',
        owner: 'Design Systems',
        status: 'ready',
      },
      {
        value: 'billing-api',
        label: 'Billing API',
        description: 'Waiting on staging credentials',
        owner: 'Platform',
        status: 'blocked',
      },
    ],
  },
]

export function CustomItemRender() {
  return (
    <div class="max-w-full w-lg">
      <CommandPalette<TeamCommand>
        groups={GROUPS}
        autofocus={false}
        itemRender={(ctx) => (
          <div class="flex flex-1 gap-3 min-w-0 items-center">
            <Icon name="i-lucide-folder-kanban text-muted-foreground group-data-[highlighted]:text-accent-foreground shrink-0" />
            <span class="flex flex-1 flex-col min-w-0">
              <span class="text-sm font-medium truncate">{ctx.item.label}</span>
              <span class="text-xs text-muted-foreground truncate group-data-[highlighted]:text-accent-foreground">
                {ctx.item.owner} · {ctx.item.description}
              </span>
            </span>
            <Badge variant={ctx.item.status === 'ready' ? 'subtle' : 'outline'}>
              {ctx.item.status}
            </Badge>
          </div>
        )}
      />
    </div>
  )
}

Custom empty state#

import { CommandPalette, Icon } from 'moraine'

export function CustomEmptyState() {
  return (
    <div class="max-w-full w-lg">
      <CommandPalette
        groups={[]}
        autofocus={false}
        emptyRender={() => (
          <span class="flex flex-col gap-2 items-center">
            <Icon name="i-lucide-search-x" class="text-xl text-muted-foreground" />
            <span class="text-foreground font-medium">No commands found</span>
            <span class="text-xs">Try a different keyword or clear the search.</span>
          </span>
        )}
      />
    </div>
  )
}

Loading#

import { CommandPalette, Icon, KbdGroup } from 'moraine'
import type { CommandPaletteT } from 'moraine'

export function Loading() {
  const BASIC_GROUPS: CommandPaletteT.Group[] = [
    {
      id: 'workspace',
      label: 'Workspace',
      items: [
        {
          value: 'new-issue',
          label: 'New Issue',
          leadingRender: () => <Icon name="i-lucide-circle-plus" />,
          trailingRender: () => <KbdGroup items={['⌘', 'N']} />,
        },
        {
          value: 'open-inbox',
          label: 'Open Inbox',
          leadingRender: () => <Icon name="i-lucide-inbox" />,
          trailingRender: () => <KbdGroup items={['⌘', 'I']} />,
        },
        {
          value: 'sync-roadmap',
          label: 'Sync Roadmap',
          leadingRender: () => <Icon name="i-lucide-refresh-cw" />,
          description: 'Pull the latest planning updates',
        },
      ],
    },
  ]

  return (
    <div class="max-w-full w-lg">
      <CommandPalette groups={BASIC_GROUPS} autofocus={false} loading />
    </div>
  )
}
import { Button, CommandPalette, Icon } from 'moraine'
import type { CommandPaletteT } from 'moraine'
import { createMemo, createSignal } from 'solid-js'

export function SubNavigation() {
  const ROOT_GROUPS: CommandPaletteT.Group[] = [
    {
      id: 'main',
      label: 'Commands',
      items: [
        {
          value: 'create',
          label: 'Create',
          leadingRender: () => <Icon name="i-lucide-plus-circle" />,
          description: 'Create new resources',
        },
        {
          value: 'share',
          label: 'Share',
          leadingRender: () => <Icon name="i-lucide-share-2" />,
          description: 'Share with others',
        },
        {
          value: 'delete',
          label: 'Delete',
          leadingRender: () => <Icon name="i-lucide-trash-2" />,
        },
      ],
    },
  ]
  const CREATE_GROUPS: CommandPaletteT.Group[] = [
    {
      id: 'create',
      label: 'Create',
      items: [
        {
          value: 'create-new-file',
          label: 'New File',
          leadingRender: () => <Icon name="i-lucide-file-plus" />,
        },
        {
          value: 'create-new-folder',
          label: 'New Folder',
          leadingRender: () => <Icon name="i-lucide-folder-plus" />,
        },
        {
          value: 'create-new-project',
          label: 'New Project',
          leadingRender: () => <Icon name="i-lucide-git-branch" />,
        },
      ],
    },
  ]
  const SHARE_GROUPS: CommandPaletteT.Group[] = [
    {
      id: 'share',
      label: 'Share',
      items: [
        {
          value: 'share-copy-link',
          label: 'Copy Link',
          leadingRender: () => <Icon name="i-lucide-link" />,
          trailingRender: () => <span class="text-xs text-muted-foreground">⌘L</span>,
        },
        {
          value: 'share-send-email',
          label: 'Send via Email',
          leadingRender: () => <Icon name="i-lucide-mail" />,
        },
      ],
    },
  ]
  const [view, setView] = createSignal<'root' | 'create' | 'share'>('root')

  const groups = createMemo(() => {
    switch (view()) {
      case 'create':
        return CREATE_GROUPS
      case 'share':
        return SHARE_GROUPS
      case 'root':
      default:
        return ROOT_GROUPS
    }
  })

  const onSelect = (item: CommandPaletteT.Item) => {
    if (item.value === 'create') {
      setView('create')
    } else if (item.value === 'share') {
      setView('share')
    }
  }

  return (
    <div class="flex flex-col gap-3 max-w-full w-lg">
      <div class="flex gap-3 items-center justify-between">
        <p class="text-sm text-muted-foreground">
          Drive multi-step navigation outside the component by swapping the `groups` prop.
        </p>
        <Button
          size="sm"
          variant="outline"
          disabled={view() === 'root'}
          onClick={() => setView('root')}
        >
          Back
        </Button>
      </div>
      <CommandPalette
        groups={groups()}
        autofocus={false}
        closeOnSelect={false}
        onSelect={onSelect}
      />
    </div>
  )
}

Attributes#

Attributes
data-disabledSlot: command-palette-itemDescription: Present when the component, slot, or item is disabled.
data-highlightedSlot: command-palette-item, command-palette-item-leading, command-palette-item-description, command-palette-item-trailingDescription: Present when the item is highlighted by pointer or keyboard navigation.
data-loadingSlot: command-palette-input-leadingDescription: Present when the component or async operation is loading.

Props#

Renders a <div> element by default.

Prop