# Command bar (Notchset): prompt.md (v2.0.0)

- id: `command-bar` · version 2.0.0 · block · free
- category: Marketing
- build: one set of files for every shadcn style
- install: `npx shadcn@latest add https://notchset.dev/r/command-bar.json`
- npm dependencies: lucide-react@^1
- registry dependencies: utils, https://notchset.dev/r/notchset-foundation.json
- docs: https://notchset.dev/blocks/command-bar
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

Say what you want in plain words and see what it will do first: the words turn into chips and one line says the consequence. Enter runs it, Tab completes, Esc clears. The host writes the verbs, so nothing is guessed..

## Build it
- Stack: React 18.3 or 19, TypeScript, Tailwind CSS v4 utility classes on the foundation's roles, a components.json (imports use its aliases).
- Packages: `lucide-react@^1`.
- Files: `components/ui/notchset/blocks/command-bar.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `notchset-foundation`.
- Builds: one set of files, the same for every shadcn style.
- Exports to keep: `CommandBar`, and every exported type.
- CSS: the install adds the notchset foundation (its roles, keyframes and motion switch, reading your shadcn tokens for colour) to your global stylesheet once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- This item reads `--ns-canvas`, `--ns-ease`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-lift`, `--ns-line`, `--ns-ok-ink`, `--ns-radius-lg`, `--ns-raised`, `--ns-signal`, `--ns-text-body`, `--ns-text-meta`, `--ns-text-small`, `--ns-time-color`, `--ns-time-menu`, `--ns-weight-medium`.
- Retune it (on any ancestor) with the foundation's roles and hooks: `--ns-bad`, `--ns-bad-ink`, `--ns-band`, `--ns-band-edge`, `--ns-band-grade`, `--ns-band-optimal`, `--ns-beat`, `--ns-bezel`, `--ns-canvas`, `--ns-cat-1`, `--ns-cat-2`, `--ns-cat-3`, `--ns-cat-4`, `--ns-cat-5`, `--ns-cat-6`, `--ns-check`, `--ns-chip`, `--ns-control`, `--ns-control-pad`, `--ns-control-sm`, `--ns-deep`, `--ns-deep-ink`, `--ns-deep-ink-2`, `--ns-deep-line`, `--ns-dialog`, `--ns-dialog-lg`, `--ns-dialog-sm`, `--ns-ease`, `--ns-ease-in`, `--ns-ease-panel`, `--ns-ease-pop`, `--ns-field`, `--ns-flood-arc`, `--ns-flood-aurora`, `--ns-flood-behind-1`, `--ns-flood-behind-2`, `--ns-flood-bloom`, `--ns-flood-deep`, `--ns-flood-dial-bloom`, `--ns-flood-glass`, `--ns-flood-grain`, `--ns-flood-image`, `--ns-flood-ink`, `--ns-flood-ink-low`, `--ns-flood-light`, `--ns-flood-mid`, `--ns-flood-pale`, `--ns-flood-pane`, `--ns-flood-pane-edge`, `--ns-flood-pill-ink`, `--ns-flood-ribbon`, `--ns-flood-ribbon-core`, `--ns-flood-shadow`, `--ns-flood-sheen`, `--ns-flood-wash`, `--ns-font-body`, `--ns-font-display`, `--ns-font-mono`, `--ns-font-script`, `--ns-glass`, `--ns-glass-blur`, `--ns-glass-shadow`, `--ns-glow`, `--ns-grain`, `--ns-gutter`, `--ns-header`, `--ns-heat`, `--ns-hover`, `--ns-ink`, `--ns-ink-2`, `--ns-ink-3`, `--ns-ink-4`, `--ns-leading`, `--ns-lift`, `--ns-light-1`, `--ns-light-2`, `--ns-light-3`, `--ns-light-spread`, `--ns-light-strength`, `--ns-line`, `--ns-line-soft`, `--ns-lit-edge`, `--ns-menu-dy`, `--ns-menu-row`, `--ns-mesh`, `--ns-mesh-frost`, `--ns-ok`, `--ns-ok-ink`, `--ns-orb-body`, `--ns-orb-cool`, `--ns-orb-core`, `--ns-orb-dawn`, `--ns-orb-deep`, `--ns-orb-edge`, `--ns-orb-grain`, `--ns-orb-ground`, `--ns-orb-halo`, `--ns-orb-inner`, `--ns-orb-key`, `--ns-orb-lift`, `--ns-orb-limb`, `--ns-orb-limb-from`, `--ns-orb-rim`, `--ns-orb-skin-hi`, `--ns-orb-skin-lo`, `--ns-orb-skin-mid`, `--ns-orb-warm`, `--ns-panel`, `--ns-panel-w`, `--ns-prism-edge`, `--ns-prism-glint`, `--ns-prism-sheen`, `--ns-prism-tint`, `--ns-pulse`, `--ns-radius`, `--ns-radius-card`, `--ns-radius-full`, `--ns-radius-lg`, `--ns-radius-sm`, `--ns-radius-xl`, `--ns-raised`, `--ns-range`, `--ns-row`, `--ns-row-compact`, `--ns-segment`, `--ns-selected`, `--ns-series`, `--ns-series-core`, `--ns-series-lilac`, `--ns-series-tint`, `--ns-shadow-dialog`, `--ns-shadow-float`, `--ns-shadow-panel`, `--ns-sheet`, `--ns-sheet-bottom`, `--ns-side`, `--ns-signal`, `--ns-sleep-awake`, `--ns-sleep-deep`, `--ns-sleep-light`, `--ns-sleep-rem`, `--ns-space-1`, `--ns-space-2`, `--ns-space-3`, `--ns-space-4`, `--ns-space-5`, `--ns-space-6`, `--ns-space-8`, `--ns-space-section`, `--ns-stack-on`, `--ns-standing-critical`, `--ns-standing-in`, `--ns-standing-out`, `--ns-state-doing`, `--ns-state-done`, `--ns-state-review`, `--ns-state-todo`, `--ns-switch-h`, `--ns-switch-w`, `--ns-text-body`, `--ns-text-control`, `--ns-text-display`, `--ns-text-figure`, `--ns-text-headline`, `--ns-text-hero`, `--ns-text-lede`, `--ns-text-meta`, `--ns-text-page`, `--ns-text-record`, `--ns-text-small`, `--ns-text-strong`, `--ns-text-sub`, `--ns-text-title`, `--ns-tile-shadow`, `--ns-time-born`, `--ns-time-color`, `--ns-time-dialog`, `--ns-time-menu`, `--ns-time-page`, `--ns-time-panel`, `--ns-time-toast`, `--ns-tint`, `--ns-tint-ink`, `--ns-toast-bottom`, `--ns-toast-left`, `--ns-touch`, `--ns-tracking-display`, `--ns-tracking-headline`, `--ns-warn`, `--ns-warn-ink`, `--ns-wash-clay`, `--ns-wash-dusk`, `--ns-wash-graphite`, `--ns-wash-ink`, `--ns-wash-sage`, `--ns-wash-sand`, `--ns-weight-light`, `--ns-weight-medium`, `--ns-weight-regular`, `--ns-weight-strong`, `--ns-well`.
- Never add Tailwind colour classes inside the component.

```tsx
import { CommandBar, type CommandVerb } from "@/components/ui/notchset/blocks/command-bar";
```

## Parts
- A single export that takes props.

## Match the original
- Read `components/ui/notchset/blocks/command-bar.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when

### Not when

## Mistakes
- Recreating the component instead of installing it

## Usage

```tsx
"use client";
import * as React from "react";
import { CommandBar, type CommandVerb } from "@/components/ui/notchset/blocks/command-bar";

const STAGES = ["Discovery", "Evaluation", "Proposal", "Security review", "Procurement"] as const;
type Stage = (typeof STAGES)[number];
/* the share of a deal's value its stage counts toward the forecast */
const WEIGHT: Record<Stage, number> = { Discovery: 0.1, Evaluation: 0.25, Proposal: 0.5, "Security review": 0.7, Procurement: 0.9 };
const k = (v: number) => `$${Math.round(v / 1000)}k`;
const startsWith = (word: string) => (x: string) => x.toLowerCase().startsWith(word.toLowerCase());

export default function CommandBarDemo() {
  const [deals, setDeals] = React.useState<{ name: string; value: number; stage: Stage }[]>([
    { name: "Webflow", value: 214500, stage: "Security review" },
    { name: "Airtable", value: 118000, stage: "Discovery" },
    { name: "Stripe", value: 126000, stage: "Security review" },
  ]);
  const move: CommandVerb<{ i: number; to: Stage }> = {
    id: "move",
    example: "move webflow to procurement",
    parse: (q) => {
      const m = /^move\s+(\w+)\s+to\s+(.+)$/i.exec(q);
      if (!m) return null;
      const i = deals.findIndex((d) => startsWith(m[1]!)(d.name));
      const to = STAGES.find(startsWith(m[2]!.trim()));
      if (i < 0 || !to) return null;
      return { parts: [{ text: "Move", kind: "verb" }, { text: deals[i]!.name }, { text: to, kind: "target" }], value: { i, to } };
    },
    preview: ({ i, to }) => {
      const d = deals[i]!;
      return `${d.stage} → ${to} · weighted ${k(d.value * WEIGHT[d.stage])} → ${k(d.value * WEIGHT[to])}`;
    },
    complete: (q) => {
      const m = /^move\s+(\w*)$/i.exec(q);
      const d = m && deals.find((x) => startsWith(m[1]!)(x.name));
      return d ? `move ${d.name.toLowerCase()} to ` : /^m(o(ve?)?)?$/i.test(q) ? "move " : null;
    },
    run: ({ i, to }) => setDeals((ds) => ds.map((d, j) => (j === i ? { ...d, stage: to } : d))),
  };
  return (
    <div className="flex w-full max-w-[560px] flex-col gap-4">
      <CommandBar label="Tell the CRM what to do" verbs={[move]} />
      <ul className="m-0 flex list-none flex-col p-0 text-[length:var(--ns-text-small)]">
        {deals.map((d) => (
          <li key={d.name} className="flex items-center gap-2.5 border-b border-[var(--ns-line-soft)] py-2 last:border-b-0">
            <span className="flex-1">{d.name}</span>
            <span className="text-[var(--ns-ink-3)]">{d.stage}</span>
            <span className="w-16 text-right tabular-nums">{k(d.value * WEIGHT[d.stage])}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `label` | `string` |  | Required. |
| `verbs` | `CommandVerb[]` |  | each verb carries its own value type (preview and run are methods, so a CommandVerb<X> is a CommandVerb) |
| `placeholder` | `string` | `"Type what you want to do…"` |  |

Full docs: https://notchset.dev/blocks/command-bar

## Customising
- Colours: every part reads the foundation's roles (`--ns-canvas`, `--ns-ink`, `--ns-line`, `--ns-signal` …), which read your shadcn tokens (`--background`, `--foreground`, `--border`, `--destructive` …), plus the optional `--success` and `--warning`. Retheme on `:root` or any container; never add Tailwind colour classes inside the component.
- Dark mode follows the `.dark` class or `data-theme="dark"` on an ancestor (the shadcn and next-themes conventions).
- Update later: Commit or stash local changes first (or copy the files), so the update can be undone. Read https://notchset.dev/r/changelog.json for the new versions: a major version, an entry marked breaking or a merge says what to change. Then re-run the install with `--overwrite`, on a branch. It replaces the component's files, the shared helper files they import, and the item's CSS rules in the global stylesheet, so review the whole diff (components, shared files, CSS), re-apply your own edits, and run the app's typecheck, tests and build.

## Accessibility contract (preserve when editing)

## Install

```bash
npx shadcn@latest add https://notchset.dev/r/command-bar.json
```

Or add the `@notchset` registry to components.json and run `npx shadcn@latest add @notchset/command-bar`. Every components.json style gets the same files.

## Continue the workflow

- [Sidebar](https://notchset.dev/blocks/sidebar)
- [Top bar](https://notchset.dev/blocks/top-bar)
- [Command menu](https://notchset.dev/components/command-menu)

## Source

### components/ui/notchset/blocks/command-bar.tsx

```tsx
// Notchset command-bar 2.0.0 · https://notchset.dev/changelog
"use client";
import * as React from "react";
import { Check, CornerDownLeft, Sparkle } from "lucide-react";
import { cn } from "@/lib/utils";

/*
 * Command bar: say what you want in plain words and see what it will do before it does it. The host registers
 * verbs, each with a parser and a preview; typing "move stripe to procurement" turns into chips (Move · Stripe ·
 * → Procurement) and one line of consequence ("Weighted $193k → $214k"). Enter runs it, Tab completes the word
 * the verb suggests, Esc clears. It's pattern matching the host writes, not a guess: a sentence that doesn't
 * match says so and shows what does.
 *
 *   const move: CommandVerb<{ deal: Deal; stage: Stage }> = {
 *     id: "move", example: "move stripe to procurement",
 *     parse: (q) => ..., preview: (v) => `Weighted ${a} → ${b}`, run: (v) => setStage(v.deal.id, v.stage) };
 *   <CommandBar label="Do something" verbs={[move]} />
 */
type CommandPart = { text: string; kind?: "verb" | "object" | "target" | "value" | undefined };
type CommandMatch<T> = { parts: CommandPart[]; value: T };
type CommandVerb<T = unknown> = {
  id: string;
  /** a sentence that works, shown when nothing matches */
  example: string;
  /** the words, or null when this verb doesn't read them */
  parse: (query: string) => CommandMatch<T> | null;
  /** one line saying what will happen */
  preview?(value: T): React.ReactNode;
  /** the rest of the sentence Tab fills in, from what's typed so far (or null) */
  complete?: ((query: string) => string | null) | undefined;
  /** runs it; false means it was refused and the words stay */
  run(value: T): void | boolean;
};
type CommandBarProps = {
  label: string;
  /** each verb carries its own value type (preview and run are methods, so a CommandVerb<X> is a CommandVerb) */
  verbs: CommandVerb[];
  placeholder?: string | undefined;
  className?: string | undefined;
};

const CHIP: Record<NonNullable<CommandPart["kind"]>, string> = {
  verb: "bg-[var(--ns-ink)] text-[var(--ns-canvas)]",
  object: "bg-[var(--ns-raised)] text-[var(--ns-ink)] shadow-[inset_0_0_0_1px_var(--ns-line)]",
  target: "bg-[var(--ns-raised)] text-[var(--ns-ink)] shadow-[inset_0_0_0_1px_var(--ns-line)]",
  value: "bg-[var(--ns-raised)] text-[var(--ns-ink)] shadow-[inset_0_0_0_1px_var(--ns-line)] tabular-nums",
};

function CommandBar({ label, verbs, placeholder = "Type what you want to do…", className }: CommandBarProps) {
  const [q, setQ] = React.useState("");
  const [done, setDone] = React.useState<string | null>(null);
  const input = React.useRef<HTMLInputElement>(null);
  const hintId = React.useId();
  const query = q.trim();
  /* the first verb that reads the words wins; the host orders them */
  let found: { verb: CommandVerb<unknown>; match: CommandMatch<unknown> } | null = null;
  for (const verb of verbs) {
    const match = query ? verb.parse(query) : null;
    if (match) {
      found = { verb, match };
      break;
    }
  }
  const completion = query && !found ? verbs.map((v) => v.complete?.(q) ?? null).find((x): x is string => !!x && x.toLowerCase() !== q.toLowerCase()) : null;
  const run = () => {
    if (!found) return;
    const ok = found.verb.run(found.match.value);
    if (ok === false) return;
    setDone(found.match.parts.map((p) => p.text).join(" "));
    setQ("");
  };
  return (
    <div data-slot="command-bar" className={cn("flex min-w-0 flex-col gap-2", className)}>
      <label className="flex h-11 items-center gap-2.5 rounded-[var(--ns-radius-lg)] bg-[var(--ns-raised)] px-3.5 shadow-[inset_0_0_0_1px_var(--ns-line),var(--ns-lift)] [transition:box-shadow_var(--ns-time-color)] focus-within:shadow-[inset_0_0_0_1.5px_var(--ns-signal),var(--ns-lift)]">
        <Sparkle aria-hidden className="size-3.5 shrink-0 text-[var(--ns-ink-3)]" />
        <span className="sr-only">{label}</span>
        <span className="relative min-w-0 flex-1">
          {/* the rest of the sentence Tab would fill in, ghosted behind the words */}
          {completion && completion.toLowerCase().startsWith(q.toLowerCase()) && (
            <span aria-hidden className="pointer-events-none absolute inset-0 truncate whitespace-pre text-[length:var(--ns-text-body)] text-[var(--ns-ink-3)] opacity-60">
              <span className="invisible">{q}</span>
              {completion.slice(q.length)}
            </span>
          )}
          <input
            ref={input}
            value={q}
            placeholder={placeholder}
            aria-describedby={hintId}
            autoComplete="off"
            spellCheck={false}
            onChange={(e) => {
              setQ(e.target.value);
              setDone(null);
            }}
            onKeyDown={(e) => {
              /* while an input method is composing, Enter and Escape confirm or cancel its candidate: never a run or a clear */
              if (e.nativeEvent.isComposing || e.keyCode === 229) return;
              if (e.key === "Enter") {
                e.preventDefault();
                run();
              } else if (e.key === "Escape") {
                if (q) e.preventDefault();
                setQ("");
                setDone(null);
              } else if (e.key === "Tab" && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey && completion) {
                /* only a plain Tab completes; Shift+Tab is the way back to the previous control */
                e.preventDefault();
                setQ(completion);
              }
            }}
            className="relative w-full bg-transparent text-[length:var(--ns-text-body)] text-[var(--ns-ink)] outline-hidden placeholder:text-[var(--ns-ink-3)] pointer-coarse:text-[16px]"
          />
        </span>
        <kbd aria-hidden className={cn("inline-flex h-6 items-center gap-1 rounded-[6px] px-1.5 text-[length:var(--ns-text-meta)] text-[var(--ns-ink-3)] shadow-[inset_0_0_0_1px_var(--ns-line)] [transition:opacity_var(--ns-time-color)] pointer-coarse:hidden", !found && "opacity-0")}>
          <CornerDownLeft className="size-3" />
          Run
        </kbd>
      </label>
      {/* what the words mean and what they'll do, or what does work */}
      <div id={hintId} aria-live="polite" className="flex min-h-14 flex-col gap-1.5 px-1">
        {found ? (
          <>
            <span className="flex flex-wrap items-center gap-1.5">
              {found.match.parts.map((p, i) => (
                <span
                  key={`${i}-${p.text}`}
                  data-ns-moves=""
                  style={{ animationDelay: `${i * 40}ms` }}
                  className={cn("inline-flex h-6 items-center rounded-[6px] px-2 text-[length:var(--ns-text-small)] font-[number:var(--ns-weight-medium)] [animation:ns-rise_var(--ns-time-menu)_var(--ns-ease)_both]", CHIP[p.kind ?? "object"])}
                >
                  {p.kind === "target" ? `→ ${p.text}` : p.text}
                </span>
              ))}
            </span>
            {found.verb.preview && <span className="text-[length:var(--ns-text-small)] text-[var(--ns-ink-2)] tabular-nums">{found.verb.preview(found.match.value)}</span>}
          </>
        ) : done ? (
          <span data-ns-moves="" className="inline-flex items-center gap-1.5 text-[length:var(--ns-text-small)] text-[var(--ns-ok-ink)] [animation:ns-rise_var(--ns-time-menu)_var(--ns-ease)]">
            <Check aria-hidden className="size-3.5" />
            Done: {done}
          </span>
        ) : query ? (
          <span className="text-[length:var(--ns-text-small)] text-[var(--ns-ink-3)]">
            Nothing reads that yet. Try “{verbs[0]?.example}”{completion ? <>, or press Tab</> : null}.
          </span>
        ) : (
          <span className="text-[length:var(--ns-text-small)] text-[var(--ns-ink-3)]">
            Try {verbs.slice(0, 2).map((v, i) => (
              <React.Fragment key={v.id}>
                {i > 0 && " or "}
                <button type="button" onClick={() => (setQ(v.example), input.current?.focus())} className="cursor-pointer rounded-[4px] text-[var(--ns-ink-2)] underline decoration-[var(--ns-line)] underline-offset-[3px] outline-hidden hover:text-[var(--ns-ink)] focus-visible:outline-2 focus-visible:outline-[var(--ns-signal)]">
                  “{v.example}”
                </button>
              </React.Fragment>
            ))}
          </span>
        )}
      </div>
    </div>
  );
}

export { CommandBar, type CommandBarProps, type CommandMatch, type CommandPart, type CommandVerb };
```

The foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://notchset.dev/r/notchset-foundation.json.
