Skip to content

Command-driven navigation

Almost no chrome — a brand, a visible ⌘K affordance, and a palette that is the real navigation.

Navbarsadvancedcommandpalettekeyboardminimal

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

'use client'

import { useState } from 'react'
import Link from 'next/link'
import { Command, Blocks, Component, FileText, Layers } from 'lucide-react'
import { BrandMark } from '@/components/library/brand'
import { CommandMenu, type CommandItem } from '@/components/ui/command-menu'
import { EmptyState } from '@/components/ui/empty-state'
import { Search } from 'lucide-react'

/**
 * Command-driven navigation
 *
 * Almost no chrome: a brand, one keyboard affordance, and a palette that is
 * the real navigation. Suited to tools whose users live on the keyboard.
 *
 * The visible ⌘K button matters — a keyboard-only entry point is undiscoverable
 * for everyone who does not already know the shortcut exists.
 */
const destinations: CommandItem[] = [
  {
    id: 'components',
    label: 'Components',
    description: '45 primitives',
    group: 'Catalogue',
    icon: <Component className="size-4" />,
    onSelect: () => {},
  },
  {
    id: 'sections',
    label: 'Sections',
    description: 'Composable page blocks',
    group: 'Catalogue',
    icon: <Layers className="size-4" />,
    onSelect: () => {},
  },
  {
    id: 'starters',
    label: 'Starters',
    description: 'Complete products',
    group: 'Catalogue',
    icon: <Blocks className="size-4" />,
    onSelect: () => {},
  },
  {
    id: 'tokens',
    label: 'Design tokens',
    description: 'The full manifest',
    group: 'Documentation',
    icon: <FileText className="size-4" />,
    onSelect: () => {},
  },
  {
    id: 'a11y',
    label: 'Accessibility',
    description: 'Contracts and testing',
    group: 'Documentation',
    icon: <FileText className="size-4" />,
    onSelect: () => {},
  },
]

export default function CommandDrivenNavigation() {
  const [open, setOpen] = useState(false)

  return (
    <div className="w-full">
      <div className="border-b border-line bg-surface">
        <div className="mx-auto flex h-14 w-full max-w-5xl items-center gap-4 px-4 sm:px-6">
          <Link href="/" className="flex items-center gap-2 text-ink-strong">
            <BrandMark className="size-5 text-accent" />
            <span className="display-type text-md font-semibold">Foundry</span>
          </Link>

          <button
            type="button"
            onClick={() => setOpen(true)}
            aria-keyshortcuts="Meta+K Control+K"
            className="ml-auto flex h-8 items-center gap-2 rounded-md border border-line bg-surface-sunken px-2.5 text-sm text-ink-subtle transition-colors hover:border-line-strong"
          >
            <Command className="size-3.5" aria-hidden="true" />
            <span className="hidden sm:inline">Jump to…</span>
            <kbd className="rounded-sm border border-line bg-surface px-1.5 py-0.5 font-mono text-2xs">
              ⌘K
            </kbd>
          </button>
        </div>
      </div>

      <div className="mx-auto w-full max-w-5xl px-4 py-12 sm:px-6">
        <p className="label-caps text-ink-subtle">Command-driven</p>
        <h2 className="display-type mt-2 text-2xl font-semibold text-ink-strong">
          The palette is the navigation.
        </h2>
        <p className="mt-3 max-w-prose text-sm text-ink-muted">
          Keep a visible trigger even when the shortcut is the point — an entry point nobody can see
          is an entry point most people never use.
        </p>
      </div>

      <CommandMenu
        open={open}
        onClose={() => setOpen(false)}
        items={destinations}
        label="Jump to"
        placeholder="Jump to a destination…"
        emptyState={
          <EmptyState
            appearance="bare"
            size="sm"
            icon={<Search className="size-5" />}
            title="Nothing matches"
            description="Try a section name such as “tokens” or “starters”."
          />
        }
      />
    </div>
  )
}

components/blocks/navigation/command-driven.tsx

'use client'

import { useState } from 'react'
import Link from 'next/link'
import { Command, Blocks, Component, FileText, Layers } from 'lucide-react'
import { BrandMark } from '@/components/library/brand'
import { CommandMenu, type CommandItem } from '@/components/ui/command-menu'
import { EmptyState } from '@/components/ui/empty-state'
import { Search } from 'lucide-react'

/**
 * Command-driven navigation
 *
 * Almost no chrome: a brand, one keyboard affordance, and a palette that is
 * the real navigation. Suited to tools whose users live on the keyboard.
 *
 * The visible ⌘K button matters — a keyboard-only entry point is undiscoverable
 * for everyone who does not already know the shortcut exists.
 */
const destinations: CommandItem[] = [
  {
    id: 'components',
    label: 'Components',
    description: '45 primitives',
    group: 'Catalogue',
    icon: <Component className="size-4" />,
    onSelect: () => {},
  },
  {
    id: 'sections',
    label: 'Sections',
    description: 'Composable page blocks',
    group: 'Catalogue',
    icon: <Layers className="size-4" />,
    onSelect: () => {},
  },
  {
    id: 'starters',
    label: 'Starters',
    description: 'Complete products',
    group: 'Catalogue',
    icon: <Blocks className="size-4" />,
    onSelect: () => {},
  },
  {
    id: 'tokens',
    label: 'Design tokens',
    description: 'The full manifest',
    group: 'Documentation',
    icon: <FileText className="size-4" />,
    onSelect: () => {},
  },
  {
    id: 'a11y',
    label: 'Accessibility',
    description: 'Contracts and testing',
    group: 'Documentation',
    icon: <FileText className="size-4" />,
    onSelect: () => {},
  },
]

export default function CommandDrivenNavigation() {
  const [open, setOpen] = useState(false)

  return (
    <div className="w-full">
      <div className="border-b border-line bg-surface">
        <div className="mx-auto flex h-14 w-full max-w-5xl items-center gap-4 px-4 sm:px-6">
          <Link href="/" className="flex items-center gap-2 text-ink-strong">
            <BrandMark className="size-5 text-accent" />
            <span className="display-type text-md font-semibold">Foundry</span>
          </Link>

          <button
            type="button"
            onClick={() => setOpen(true)}
            aria-keyshortcuts="Meta+K Control+K"
            className="ml-auto flex h-8 items-center gap-2 rounded-md border border-line bg-surface-sunken px-2.5 text-sm text-ink-subtle transition-colors hover:border-line-strong"
          >
            <Command className="size-3.5" aria-hidden="true" />
            <span className="hidden sm:inline">Jump to…</span>
            <kbd className="rounded-sm border border-line bg-surface px-1.5 py-0.5 font-mono text-2xs">
              ⌘K
            </kbd>
          </button>
        </div>
      </div>

      <div className="mx-auto w-full max-w-5xl px-4 py-12 sm:px-6">
        <p className="label-caps text-ink-subtle">Command-driven</p>
        <h2 className="display-type mt-2 text-2xl font-semibold text-ink-strong">
          The palette is the navigation.
        </h2>
        <p className="mt-3 max-w-prose text-sm text-ink-muted">
          Keep a visible trigger even when the shortcut is the point — an entry point nobody can see
          is an entry point most people never use.
        </p>
      </div>

      <CommandMenu
        open={open}
        onClose={() => setOpen(false)}
        items={destinations}
        label="Jump to"
        placeholder="Jump to a destination…"
        emptyState={
          <EmptyState
            appearance="bare"
            size="sm"
            icon={<Search className="size-5" />}
            title="Nothing matches"
            description="Try a section name such as “tokens” or “starters”."
          />
        }
      />
    </div>
  )
}

components/ui/command-menu.tsx

'use client'

import {
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type KeyboardEvent,
  type ReactNode,
} from 'react'
import { Search, CornerDownLeft, ArrowUp, ArrowDown } from 'lucide-react'
import { cn } from '@/lib/cn'
import { useFocusTrap } from '@/hooks/use-focus-trap'
import { useDismiss, useScrollLock } from '@/hooks/use-dismiss'
import { Portal } from './portal'

/**
 * CommandMenu
 *
 * A modal command palette implementing the combobox-in-a-dialog pattern: the
 * dialog owns the modality, the input owns `role="combobox"` and
 * `aria-activedescendant`, and the results are a real listbox.
 *
 * Focus stays in the input for the whole interaction — arrow keys move the
 * *active descendant*, not DOM focus — so typing never gets interrupted.
 * Every item is selectable with Enter, and the active option is scrolled into
 * view without moving the page behind the overlay.
 */
export interface CommandItem {
  id: string
  label: string
  description?: string
  group: string
  icon?: ReactNode
  meta?: string
  keywords?: string[]
  onSelect: () => void
}

export interface CommandMenuProps {
  open: boolean
  onClose: () => void
  items: CommandItem[]
  placeholder?: string
  /** Rendered when the query has no matches. */
  emptyState?: ReactNode
  /** Shown while the query is empty — typically recent or suggested items. */
  initialItems?: CommandItem[]
  label?: string
  footer?: ReactNode
  onQueryChange?: (query: string) => void
}

function score(item: CommandItem, query: string): number {
  const q = query.toLowerCase()
  const label = item.label.toLowerCase()
  if (label === q) return 100
  if (label.startsWith(q)) return 80
  if (label.includes(q)) return 60
  if (item.keywords?.some((keyword) => keyword.toLowerCase().includes(q))) return 40
  if (item.description?.toLowerCase().includes(q)) return 20
  if (item.group.toLowerCase().includes(q)) return 10
  return 0
}

export function CommandMenu({
  open,
  onClose,
  items,
  placeholder = 'Search…',
  emptyState,
  initialItems,
  label = 'Command menu',
  footer,
  onQueryChange,
}: CommandMenuProps) {
  const [query, setQuery] = useState('')
  const [activeIndex, setActiveIndex] = useState(0)
  const [lastOpen, setLastOpen] = useState(open)

  // Resetting during render rather than in an effect avoids a second render
  // pass every time the palette opens.
  if (lastOpen !== open) {
    setLastOpen(open)
    setQuery('')
    setActiveIndex(0)
  }
  const panelRef = useRef<HTMLDivElement>(null)
  const inputRef = useRef<HTMLInputElement>(null)
  const listRef = useRef<HTMLDivElement>(null)
  const uid = useId()

  useFocusTrap(panelRef, open, { initialFocus: inputRef })
  useScrollLock(open)
  useDismiss([panelRef], open, onClose)

  const results = useMemo(() => {
    const trimmed = query.trim()
    if (!trimmed) return initialItems ?? items.slice(0, 8)
    return items
      .map((item) => ({ item, value: score(item, trimmed) }))
      .filter((entry) => entry.value > 0)
      .sort((a, b) => b.value - a.value || a.item.label.localeCompare(b.item.label))
      .slice(0, 40)
      .map((entry) => entry.item)
  }, [items, initialItems, query])

  const grouped = useMemo(() => {
    const map = new Map<string, CommandItem[]>()
    for (const item of results) {
      const bucket = map.get(item.group)
      if (bucket) bucket.push(item)
      else map.set(item.group, [item])
    }
    return Array.from(map.entries())
  }, [results])

  useEffect(() => {
    if (!open) return
    const active = listRef.current?.querySelector('[data-active="true"]')
    active?.scrollIntoView({ block: 'nearest' })
  }, [activeIndex, open])

  const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
    if (event.key === 'ArrowDown') {
      event.preventDefault()
      setActiveIndex((current) => (results.length === 0 ? 0 : (current + 1) % results.length))
    } else if (event.key === 'ArrowUp') {
      event.preventDefault()
      setActiveIndex((current) =>
        results.length === 0 ? 0 : (current - 1 + results.length) % results.length,
      )
    } else if (event.key === 'Home') {
      event.preventDefault()
      setActiveIndex(0)
    } else if (event.key === 'End') {
      event.preventDefault()
      setActiveIndex(Math.max(0, results.length - 1))
    } else if (event.key === 'Enter') {
      event.preventDefault()
      const item = results[activeIndex]
      if (item) {
        item.onSelect()
        onClose()
      }
    }
  }

  if (!open) return null

  let cursor = -1

  return (
    <Portal>
      <div className="fixed inset-0 z-[120] flex items-start justify-center p-4 pt-[10vh]">
        <div className="animate-fade-in absolute inset-0 bg-scrim" aria-hidden="true" />
        <div
          ref={panelRef}
          role="dialog"
          aria-modal="true"
          aria-label={label}
          className="animate-scale-in relative flex max-h-[70vh] w-full max-w-xl flex-col overflow-hidden rounded-xl border border-line bg-surface-raised shadow-overlay"
        >
          <div className="flex items-center gap-3 border-b border-line px-4">
            <Search className="size-4 shrink-0 text-ink-subtle" aria-hidden="true" />
            <input
              ref={inputRef}
              type="text"
              role="combobox"
              autoComplete="off"
              spellCheck={false}
              aria-expanded
              aria-controls={`${uid}-list`}
              aria-autocomplete="list"
              aria-activedescendant={
                results[activeIndex] ? `${uid}-option-${results[activeIndex]?.id}` : undefined
              }
              value={query}
              placeholder={placeholder}
              onChange={(event) => {
                setQuery(event.target.value)
                setActiveIndex(0)
                onQueryChange?.(event.target.value)
              }}
              onKeyDown={onKeyDown}
              className="h-12 w-full min-w-0 border-0 bg-transparent text-sm text-ink outline-none placeholder:text-ink-subtle"
            />
            <kbd className="hidden shrink-0 rounded-sm border border-line bg-surface-sunken px-1.5 py-0.5 font-mono text-2xs text-ink-subtle sm:block">
              Esc
            </kbd>
          </div>

          <div
            ref={listRef}
            id={`${uid}-list`}
            role="listbox"
            aria-label="Results"
            className="thin-scrollbar min-h-0 flex-1 overflow-y-auto p-2"
          >
            {results.length === 0 ? (
              <div className="px-3 py-10">{emptyState}</div>
            ) : (
              grouped.map(([group, groupItems]) => (
                <div key={group} className="mb-2 last:mb-0">
                  <p className="label-caps px-2 py-1.5 text-ink-subtle">{group}</p>
                  {groupItems.map((item) => {
                    cursor += 1
                    const index = cursor
                    const active = index === activeIndex
                    return (
                      <div
                        key={item.id}
                        id={`${uid}-option-${item.id}`}
                        role="option"
                        aria-selected={active}
                        data-active={active}
                        onMouseMove={() => setActiveIndex(index)}
                        onClick={() => {
                          item.onSelect()
                          onClose()
                        }}
                        className={cn(
                          'flex cursor-pointer items-center gap-3 rounded-md px-2.5 py-2 text-sm',
                          active && 'bg-accent-soft text-accent-soft-ink',
                        )}
                      >
                        {item.icon ? (
                          <span className={cn('shrink-0', active ? '' : 'text-ink-subtle')}>
                            {item.icon}
                          </span>
                        ) : null}
                        <span className="min-w-0 flex-1">
                          <span className="block truncate font-medium">{item.label}</span>
                          {item.description ? (
                            <span
                              className={cn(
                                'block truncate text-xs',
                                active ? 'opacity-80' : 'text-ink-muted',
                              )}
                            >
                              {item.description}
                            </span>
                          ) : null}
                        </span>
                        {item.meta ? (
                          <span
                            className={cn(
                              'shrink-0 font-mono text-2xs',
                              active ? 'opacity-80' : 'text-ink-subtle',
                            )}
                          >
                            {item.meta}
                          </span>
                        ) : null}
                      </div>
                    )
                  })}
                </div>
              ))
            )}
          </div>

          <div className="flex items-center justify-between gap-3 border-t border-line bg-surface-sunken px-4 py-2">
            <div className="flex items-center gap-3 text-2xs text-ink-subtle">
              <span className="flex items-center gap-1">
                <ArrowUp className="size-3" aria-hidden="true" />
                <ArrowDown className="size-3" aria-hidden="true" />
                Navigate
              </span>
              <span className="flex items-center gap-1">
                <CornerDownLeft className="size-3" aria-hidden="true" />
                Open
              </span>
            </div>
            {footer}
          </div>
        </div>
      </div>
    </Portal>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

For tools whose users live on the keyboard. The visible trigger is the part that matters: a keyboard-only entry point is undiscoverable to everyone who does not already know it exists.

  • `aria-keyshortcuts` on the trigger advertises the binding to assistive tech.
  • Group destinations so the palette stays readable once the catalogue grows.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Visible trigger
  • Grouped destinations
  • Empty state

Accessibility

Discoverability
A visible button duplicates the shortcut, so the feature is not keyboard-expert-only.
Palette contract
Modality, focus and dismissal are handled by the CommandMenu primitive.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.