Command Nav
Navbar with command palette: search opens a real ⌘K palette with recents, grouped fuzzy results and a live preview.
- Navbars
- SaaS analytics
- React only · 0 deps
- 1 file
Install. One command, or copy the file.
$
npx shadcn@latest add https://freedesignmd.com/r/command-nav.json- Byte-identical to the preview
- No Tailwind or config needed
- SSR-safe · keyboard accessible
- Respects reduced motion & transparency
components/navbars/command-nav.tsx
1/**2 * CommandNav, a quiet product-analytics nav whose search field opens a real3 * ⌘K command palette: recent items, grouped results, fuzzy filtering with4 * highlighted matches, full keyboard control and a live preview pane that5 * draws a sparkline for the highlighted dashboard or event.6 *7 * Props: brand, workspace, links, current, commands (grouped), recent (ids),8 * onRun(item), user ({ name, src }).9 * CSS vars: --fd-cmd-paper, --fd-cmd-ink, --fd-cmd-accent.10 *11 * Keyboard: ⌘K / Ctrl+K toggles the palette anywhere, "/" opens it when you12 * are not typing, ↑ ↓ move, Home/End jump, Enter runs, Escape closes (or13 * clears the query first).14 * Browser support: all evergreen browsers.15 * Accessibility: the palette is a modal dialog; the input is a combobox16 * driving a listbox through aria-activedescendant; groups are labelled; the17 * result count is announced politely. Focus returns to the opener on close.18 * The mobile menu button has aria-expanded and closes on Escape.19 * Reduced motion: palette appears without scaling.20 */21import { useEffect, useId, useMemo, useRef, useState } from "react";22import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";23 24export type CmdKind = "dashboard" | "event" | "action" | "person";25export interface CmdItem { id: string; label: string; kind: CmdKind; hint?: string; keys?: string[]; keywords?: string }26export interface CmdGroup { heading: string; items: CmdItem[] }27export interface CommandNavProps {28 brand?: string;29 workspace?: { org: string; project: string };30 links?: { label: string; href: string }[];31 current?: string;32 commands?: CmdGroup[];33 recent?: string[];34 user?: { name: string; src: string };35 onRun?: (item: CmdItem) => void;36 className?: string;37}38 39const COMMANDS: CmdGroup[] = [40 { heading: "Dashboards", items: [41 { id: "d-checkout", label: "Checkout funnel", kind: "dashboard", hint: "Edited by Priya, 2h ago" },42 { id: "d-retention", label: "Week 1 retention by plan", kind: "dashboard", hint: "Pinned · 14 charts" },43 { id: "d-north", label: "North star: weekly riders", kind: "dashboard", hint: "Shared with leadership" },44 { id: "d-ebike", label: "E-bike rental conversion", kind: "dashboard", hint: "Created by you" },45 { id: "d-support", label: "Support tickets vs. app version", kind: "dashboard", hint: "Last viewed Monday" },46 ] },47 { heading: "Events", items: [48 { id: "e-unlock", label: "bike_unlocked", kind: "event", hint: "41,208 in 24h", keywords: "unlock ride start" },49 { id: "e-pay", label: "payment_failed", kind: "event", hint: "312 in 24h", keywords: "card error checkout" },50 { id: "e-signup", label: "signup_completed", kind: "event", hint: "1,904 in 24h", keywords: "register account" },51 { id: "e-dock", label: "dock_full_warning", kind: "event", hint: "88 in 24h", keywords: "station capacity" },52 ] },53 { heading: "Actions", items: [54 { id: "a-new", label: "New insight", kind: "action", keys: ["N"], keywords: "create chart" },55 { id: "a-funnel", label: "New funnel from events", kind: "action", keys: ["F"], keywords: "create" },56 { id: "a-invite", label: "Invite a teammate", kind: "action", keys: ["I"], keywords: "member user add" },57 { id: "a-theme", label: "Switch to dark theme", kind: "action", keys: ["⇧", "D"], keywords: "appearance mode" },58 { id: "a-export", label: "Export current view as CSV", kind: "action", keys: ["E"], keywords: "download" },59 ] },60 { heading: "People", items: [61 { id: "p-priya", label: "Priya Raman", kind: "person", hint: "Growth · owns 9 dashboards" },62 { id: "p-jonas", label: "Jonas Ekberg", kind: "person", hint: "Data engineering" },63 ] },64];65 66const LINKS = [67 { label: "Dashboards", href: "#dashboards" },68 { label: "Funnels", href: "#funnels" },69 { label: "Retention", href: "#retention" },70 { label: "Live events", href: "#live" },71];72 73/** Subsequence fuzzy match: returns score and matched indices, or null. */74function fuzzy(q: string, text: string): { score: number; idx: number[] } | null {75 const s = text.toLowerCase();76 const n = q.toLowerCase().replace(/\s+/g, " ").trim();77 if (!n) return { score: 0, idx: [] };78 let i = 0, score = 0, prev = -2;79 const idx: number[] = [];80 for (const ch of n) {81 if (ch === " ") continue;82 const j = s.indexOf(ch, i);83 if (j < 0) return null;84 const start = j === 0 || /[\s_:.\-]/.test(s[j - 1]);85 score += 1 + (j === prev + 1 ? 3 : 0) + (start ? 4 : 0) - Math.min(3, (j - i) * 0.1);86 idx.push(j);87 prev = j;88 i = j + 1;89 }90 // Reject scattered matches: accept one run, acronym-style runs, or a91 // word-start match with only a few breaks.92 const starts = idx.filter((j, k) => k === 0 || idx[k - 1] !== j - 1);93 const atWord = (j: number) => j === 0 || /[\s_:.\-]/.test(s[j - 1]);94 const ok = starts.length === 1 || starts.every(atWord) || (atWord(starts[0]) && starts.length <= 1 + Math.floor(idx.length / 3));95 if (!ok) return null;96 return { score: score - s.length * 0.02, idx };97}98 99function Hl({ text, idx }: { text: string; idx: number[] }) {100 if (!idx.length) return <>{text}</>;101 const set = new Set(idx);102 const out: ReactNode[] = [];103 let buf = "", on = false;104 for (let i = 0; i <= text.length; i++) {105 const m = i < text.length && set.has(i);106 if ((m !== on || i === text.length) && buf) {107 out.push(on ? <mark key={i}>{buf}</mark> : <span key={i}>{buf}</span>);108 buf = "";109 }110 on = m;111 if (i < text.length) buf += text[i];112 }113 return <>{out}</>;114}115 116function seeded(id: string) {117 let h = 2166136261;118 for (const c of id) h = Math.imul(h ^ c.charCodeAt(0), 16777619);119 return () => ((h = Math.imul(h ^ (h >>> 13), 1274126177)) >>> 0) / 4294967295;120}121 122function Glyph({ kind }: { kind: CmdKind }) {123 const p = { fill: "none", stroke: "currentColor", strokeWidth: 1.4, strokeLinecap: "round" as const, strokeLinejoin: "round" as const };124 return (125 <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">126 {kind === "dashboard" && <><rect x="2" y="2" width="5" height="6" rx="1" {...p} /><rect x="9" y="2" width="5" height="3.5" rx="1" {...p} /><rect x="2" y="10" width="5" height="4" rx="1" {...p} /><rect x="9" y="7.5" width="5" height="6.5" rx="1" {...p} /></>}127 {kind === "event" && <path d="M2 8h2.5l1.8-4.5 3.4 9 1.8-4.5H14" {...p} />}128 {kind === "action" && <path d="M8 2.5v11M2.5 8h11" {...p} />}129 {kind === "person" && <><circle cx="8" cy="5.5" r="2.7" {...p} /><path d="M2.8 14c.6-2.8 2.7-4.2 5.2-4.2s4.6 1.4 5.2 4.2" {...p} /></>}130 </svg>131 );132}133 134function Spark({ id }: { id: string }) {135 const pts = useMemo(() => {136 const r = seeded(id);137 let v = 40 + r() * 20;138 return Array.from({ length: 24 }, () => (v = Math.max(8, Math.min(92, v + (r() - 0.45) * 18))));139 }, [id]);140 const d = pts.map((y, i) => `${i ? "L" : "M"}${(i / 23) * 220} ${100 - y}`).join(" ");141 return (142 <svg className="fd-cmd-spark" viewBox="0 0 220 100" preserveAspectRatio="none" aria-hidden="true">143 <path d={`${d} L220 100 L0 100 Z`} fill="var(--fd-cmd-accent)" opacity="0.08" />144 <path d={d} fill="none" stroke="var(--fd-cmd-accent)" strokeWidth="1.6" vectorEffect="non-scaling-stroke" />145 </svg>146 );147}148 149export function CommandNav({150 brand = "plumb",151 workspace = { org: "Halden Bikes", project: "Production" },152 links = LINKS,153 current = "#funnels",154 commands = COMMANDS,155 recent: recentInit = ["d-checkout", "e-pay", "a-invite"],156 user = { name: "Mara Lindqvist", src: "/img/people/p6.webp" },157 onRun,158 className,159}: CommandNavProps) {160 const uid = useId();161 const [open, setOpen] = useState(false);162 const [menu, setMenu] = useState(false);163 const [q, setQ] = useState("");164 const [cur, setCur] = useState(0);165 const [recent, setRecent] = useState(recentInit);166 const [toast, setToast] = useState<string | null>(null);167 const input = useRef<HTMLInputElement>(null);168 const list = useRef<HTMLDivElement>(null);169 const opener = useRef<HTMLElement | null>(null);170 const menuBtn = useRef<HTMLButtonElement>(null);171 const menuEl = useRef<HTMLDivElement>(null);172 const pointerMoved = useRef(false);173 174 const all = useMemo(() => commands.flatMap((g) => g.items.map((i) => ({ i, g: g.heading }))), [commands]);175 176 const groups = useMemo(() => {177 if (!q.trim()) {178 const rec = recent.map((id) => all.find((x) => x.i.id === id)).filter(Boolean).map((x) => ({ item: x!.i, idx: [] as number[] }));179 const out: { heading: string; rows: { item: CmdItem; idx: number[] }[] }[] = [];180 if (rec.length) out.push({ heading: "Recent", rows: rec });181 commands.forEach((g) => {182 const rows = g.items.filter((i) => !recent.includes(i.id)).slice(0, 3).map((item) => ({ item, idx: [] as number[] }));183 if (rows.length) out.push({ heading: g.heading, rows });184 });185 return out;186 }187 return commands188 .map((g) => ({189 heading: g.heading,190 rows: g.items191 .map((item) => {192 const a = fuzzy(q, item.label);193 const b = a ? null : fuzzy(q, item.keywords ?? "");194 const m = a ?? (b ? { score: b.score - 3, idx: [] } : null);195 return m ? { item, idx: m.idx, score: m.score } : null;196 })197 .filter(Boolean)198 .sort((x, y) => y!.score - x!.score) as { item: CmdItem; idx: number[] }[],199 }))200 .filter((g) => g.rows.length)201 .sort((a, b) => ((b.rows[0] as { score?: number }).score ?? 0) - ((a.rows[0] as { score?: number }).score ?? 0));202 }, [q, commands, recent, all]);203 204 const flat = useMemo(() => groups.flatMap((g) => g.rows.map((r) => r.item)), [groups]);205 const active = flat[Math.min(cur, flat.length - 1)];206 const optId = (id: string) => `${uid}-o-${id}`;207 208 const show = (from?: HTMLElement | null) => {209 opener.current = from ?? (document.activeElement as HTMLElement);210 setMenu(false);211 setQ("");212 setCur(0);213 setOpen(true);214 };215 const hide = () => {216 setOpen(false);217 window.setTimeout(() => opener.current?.focus?.(), 0);218 };219 220 // Global shortcuts.221 useEffect(() => {222 const on = (e: KeyboardEvent) => {223 if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {224 e.preventDefault();225 if (open) hide();226 else show();227 } else if (e.key === "/" && !open) {228 const t = e.target as HTMLElement;229 if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;230 e.preventDefault();231 show();232 }233 };234 window.addEventListener("keydown", on);235 return () => window.removeEventListener("keydown", on);236 }, [open]); // eslint-disable-line react-hooks/exhaustive-deps237 238 useEffect(() => {239 if (open) window.setTimeout(() => input.current?.focus(), 10);240 }, [open]);241 useEffect(() => setCur(0), [q]);242 243 // Keep the active row in view.244 useEffect(() => {245 if (!open || !active) return;246 const el = document.getElementById(optId(active.id));247 const l = list.current;248 if (el && l) {249 const top = el.offsetTop - 34, bottom = el.offsetTop + el.offsetHeight + 8;250 if (top < l.scrollTop) l.scrollTop = top;251 else if (bottom > l.scrollTop + l.clientHeight) l.scrollTop = bottom - l.clientHeight;252 }253 }, [cur, open, active]); // eslint-disable-line react-hooks/exhaustive-deps254 255 // Toast lifetime.256 useEffect(() => {257 if (!toast) return;258 const t = window.setTimeout(() => setToast(null), 2400);259 return () => window.clearTimeout(t);260 }, [toast]);261 262 // Mobile menu.263 useEffect(() => {264 if (!menu) return;265 menuEl.current?.querySelector<HTMLElement>("a,button")?.focus();266 const on = (e: KeyboardEvent) => {267 if (e.key === "Escape") {268 setMenu(false);269 menuBtn.current?.focus();270 }271 };272 const out = (e: PointerEvent) => {273 if (!menuEl.current?.contains(e.target as Node) && !menuBtn.current?.contains(e.target as Node)) setMenu(false);274 };275 document.addEventListener("keydown", on);276 document.addEventListener("pointerdown", out);277 return () => {278 document.removeEventListener("keydown", on);279 document.removeEventListener("pointerdown", out);280 };281 }, [menu]);282 283 const run = (item: CmdItem) => {284 setRecent((r) => [item.id, ...r.filter((x) => x !== item.id)].slice(0, 4));285 onRun?.(item);286 setToast(item.kind === "action" ? `${item.label}` : `Opened ${item.label}`);287 hide();288 };289 290 const onKey = (e: ReactKeyboardEvent<HTMLInputElement>) => {291 const n = flat.length;292 if (e.key === "ArrowDown") { e.preventDefault(); if (n) setCur((c) => (c + 1) % n); }293 else if (e.key === "ArrowUp") { e.preventDefault(); if (n) setCur((c) => (c - 1 + n) % n); }294 else if (e.key === "Home") { e.preventDefault(); setCur(0); }295 else if (e.key === "End") { e.preventDefault(); setCur(Math.max(0, n - 1)); }296 else if (e.key === "Enter") { e.preventDefault(); if (active) run(active); }297 else if (e.key === "Escape") { e.preventDefault(); if (q) setQ(""); else hide(); }298 else if (e.key === "Tab") e.preventDefault();299 };300 301 let rowIndex = -1;302 const kindLabel: Record<CmdKind, string> = { dashboard: "Dashboard", event: "Event", action: "Action", person: "Person" };303 304 return (305 <header className={`fd-cmd${className ? " " + className : ""}`}>306 <style>{css}</style>307 <div className="fd-cmd-bar">308 <a href="#top" className="fd-cmd-logo" aria-label={`${brand} home`}>309 <svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path d="M10 1.5v5" stroke="currentColor" strokeWidth="1.5" /><path d="M10 6.5 15 12a5 5 0 0 1-10 0z" fill="currentColor" /><circle cx="10" cy="12.5" r="1.6" fill="var(--fd-cmd-paper)" /></svg>310 <span>{brand}</span>311 </a>312 <span className="fd-cmd-crumb" aria-label="Workspace">313 <span className="fd-cmd-slash" aria-hidden="true">/</span>314 <span className="fd-cmd-org">{workspace.org}</span>315 <span className="fd-cmd-env"><i aria-hidden="true" />{workspace.project}</span>316 </span>317 <nav className="fd-cmd-nav" aria-label="Primary">318 {links.map((l) => <a key={l.href} href={l.href} aria-current={l.href === current ? "page" : undefined}>{l.label}</a>)}319 </nav>320 <button type="button" className="fd-cmd-search" aria-haspopup="dialog" aria-expanded={open} onClick={(e) => show(e.currentTarget)}>321 <svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><circle cx="7" cy="7" r="4.8" fill="none" stroke="currentColor" strokeWidth="1.4" /><path d="M10.6 10.6 14 14" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /></svg>322 <span className="fd-cmd-ph">Search or jump to…</span>323 <kbd>⌘K</kbd>324 </button>325 <a href="#invite" className="fd-cmd-invite">Invite</a>326 <img className="fd-cmd-av" src={user.src} alt={user.name} width={32} height={32} />327 <button ref={menuBtn} type="button" className="fd-cmd-burger" aria-label="Menu" aria-expanded={menu} aria-controls={`${uid}-m`} onClick={() => setMenu((m) => !m)}>328 <svg width="18" height="18" viewBox="0 0 18 18" aria-hidden="true">{menu ? <path d="M4 4l10 10M14 4 4 14" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" /> : <path d="M3 6h12M3 12h12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />}</svg>329 </button>330 </div>331 332 <div ref={menuEl} id={`${uid}-m`} className="fd-cmd-menu" hidden={!menu}>333 <button type="button" className="fd-cmd-msearch" onClick={(e) => show(menuBtn.current ?? e.currentTarget)}>334 <svg width="15" height="15" viewBox="0 0 16 16" aria-hidden="true"><circle cx="7" cy="7" r="4.8" fill="none" stroke="currentColor" strokeWidth="1.4" /><path d="M10.6 10.6 14 14" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" /></svg>335 Search or jump to…336 </button>337 <nav aria-label="Mobile">338 {links.map((l) => <a key={l.href} href={l.href} aria-current={l.href === current ? "page" : undefined} onClick={() => setMenu(false)}>{l.label}</a>)}339 </nav>340 <div className="fd-cmd-mfoot">341 <span className="fd-cmd-org">{workspace.org}</span>342 <span className="fd-cmd-env"><i aria-hidden="true" />{workspace.project}</span>343 <a href="#invite" className="fd-cmd-invite">Invite</a>344 </div>345 </div>346 347 {open && (348 <div className="fd-cmd-layer">349 <div className="fd-cmd-scrim" aria-hidden="true" onClick={hide} />350 <div className="fd-cmd-pal" role="dialog" aria-modal="true" aria-label="Command palette">351 <div className="fd-cmd-field">352 <svg width="18" height="18" viewBox="0 0 16 16" aria-hidden="true"><circle cx="7" cy="7" r="4.8" fill="none" stroke="currentColor" strokeWidth="1.3" /><path d="M10.6 10.6 14 14" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" /></svg>353 <input354 ref={input}355 value={q}356 onChange={(e) => setQ(e.target.value)}357 onKeyDown={onKey}358 placeholder="Search dashboards, events, people, or type a command"359 role="combobox"360 aria-expanded="true"361 aria-controls={`${uid}-lb`}362 aria-activedescendant={active ? optId(active.id) : undefined}363 aria-autocomplete="list"364 spellCheck={false}365 autoComplete="off"366 />367 <button type="button" className="fd-cmd-esc" onClick={hide} aria-label="Close">esc</button>368 </div>369 <div className="fd-cmd-body">370 <div ref={list} id={`${uid}-lb`} role="listbox" aria-label="Results" className="fd-cmd-list" onPointerMove={() => (pointerMoved.current = true)}>371 {groups.map((g, gi) => (372 <div key={g.heading} role="group" aria-labelledby={`${uid}-g${gi}`} className="fd-cmd-group">373 <div id={`${uid}-g${gi}`} className="fd-cmd-gh">{g.heading}</div>374 {g.rows.map((r) => {375 rowIndex++;376 const ri = rowIndex;377 const on = ri === Math.min(cur, flat.length - 1);378 return (379 <div380 key={g.heading + r.item.id}381 id={optId(r.item.id)}382 role="option"383 aria-selected={on}384 className="fd-cmd-opt"385 data-kind={r.item.kind}386 onPointerEnter={() => pointerMoved.current && setCur(ri)}387 onMouseDown={(e) => e.preventDefault()}388 onClick={() => run(r.item)}389 >390 <span className="fd-cmd-ic">391 {r.item.kind === "person" ? <span className="fd-cmd-ini">{r.item.label.split(" ").map((w) => w[0]).join("")}</span> : <Glyph kind={r.item.kind} />}392 </span>393 <span className={`fd-cmd-lab${r.item.kind === "event" ? " fd-cmd-mono" : ""}`}><Hl text={r.item.label} idx={r.idx} /></span>394 {r.item.keys ? (395 <span className="fd-cmd-keys">{r.item.keys.map((k) => <kbd key={k}>{k}</kbd>)}</span>396 ) : (397 <span className="fd-cmd-hint">{r.item.hint}</span>398 )}399 <svg className="fd-cmd-enter" width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M11.5 3v4.5H3.5M6 5 3.5 7.5 6 10" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round" /></svg>400 </div>401 );402 })}403 </div>404 ))}405 {!flat.length && (406 <div className="fd-cmd-empty">407 <p>Nothing called “{q}”.</p>408 <span>Try an event name like <code>bike_unlocked</code>, or a teammate.</span>409 </div>410 )}411 </div>412 <aside className="fd-cmd-prev" aria-hidden="true">413 {active && (414 <>415 <span className="fd-cmd-pk">{kindLabel[active.kind]}</span>416 <p className="fd-cmd-pt">{active.label}</p>417 {(active.kind === "dashboard" || active.kind === "event") ? (418 <>419 <Spark id={active.id} />420 <div className="fd-cmd-pstats">421 {active.kind === "event" ? <span><b>{active.hint?.split(" in ")[0]}</b> in the last 24 hours</span> : <span><b>{(seeded(active.id)() * 40 + 8).toFixed(1)}%</b> conversion</span>}422 <span><b>{active.kind === "event" ? "+" : "−"}{(seeded(active.id + "x")() * 9 + 1).toFixed(1)}%</b> vs last week</span>423 </div>424 </>425 ) : (426 <p className="fd-cmd-pd">{active.kind === "action" ? `Runs immediately. Shortcut ${active.keys?.join(" ") ?? ""} works anywhere outside a text field.` : active.hint}</p>427 )}428 </>429 )}430 </aside>431 </div>432 <div className="fd-cmd-foot">433 <span><kbd>↑</kbd><kbd>↓</kbd> move</span>434 <span><kbd>↵</kbd> open</span>435 <span><kbd>esc</kbd> {q ? "clear" : "close"}</span>436 <span className="fd-cmd-count" role="status" aria-live="polite">{flat.length} {flat.length === 1 ? "result" : "results"}</span>437 </div>438 </div>439 </div>440 )}441 442 {toast && <div className="fd-cmd-toast" role="status">{toast}</div>}443 </header>444 );445}446 447const sans = `ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif`;448const mono = `ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace`;449 450const css = `451.fd-cmd {452 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);453 --fd-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);454 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);455 --fd-dur-1: 120ms; --fd-dur-2: 240ms; --fd-dur-3: 420ms; --fd-dur-4: 700ms;456 --fd-cmd-paper: #fbfbf9;457 --fd-cmd-ink: #14161a;458 --fd-cmd-soft: #6a6d74;459 --fd-cmd-line: #e6e6e1;460 --fd-cmd-accent: #2a44ff;461 position: sticky; top: 0; z-index: 50; display: block; margin: 0; padding: 0;462 font: 400 14px/1.4 ${sans};463 color: var(--fd-cmd-ink); background: var(--fd-cmd-paper);464 border-bottom: 1px solid var(--fd-cmd-line);465}466.fd-cmd, .fd-cmd *, .fd-cmd *::before, .fd-cmd *::after { box-sizing: border-box; }467.fd-cmd a:focus-visible, .fd-cmd button:focus-visible { outline: 2px solid color-mix(in srgb, var(--fd-cmd-accent) 70%, transparent); outline-offset: 2px; }468.fd-cmd kbd { display: inline-grid; place-items: center; min-width: 20px; height: 20px; margin: 0; padding: 0 5px; border: 1px solid var(--fd-cmd-line); border-bottom-width: 2px; border-radius: 5px; background: #fff; font: 500 0.6875rem/1 ${mono}; color: var(--fd-cmd-soft); }469 470.fd-cmd-bar { display: flex; align-items: center; gap: 14px; height: 60px; margin: 0; padding: 0 20px; }471.fd-cmd-logo { display: inline-flex; align-items: center; gap: 7px; margin: 0; padding: 4px; color: var(--fd-cmd-ink); text-decoration: none; border-radius: 6px; }472.fd-cmd-logo svg { display: block; }473.fd-cmd-logo span { font: 700 1.125rem/1 ${sans}; letter-spacing: -0.04em; }474.fd-cmd-crumb { display: inline-flex; align-items: center; gap: 9px; margin: 0; padding: 0; white-space: nowrap; }475.fd-cmd-slash { font: 300 1.25rem/1 ${sans}; color: #c8c8c2; }476.fd-cmd-org { font: 500 0.875rem/1 ${sans}; color: var(--fd-cmd-ink); }477.fd-cmd-env { display: inline-flex; align-items: center; gap: 6px; margin: 0; padding: 4px 8px; border-radius: 9999px; font: 500 0.75rem/1 ${mono}; color: #1d6b3a; background: #e8f4ea; }478.fd-cmd-env i { display: block; width: 6px; height: 6px; border-radius: 9999px; background: #23a455; }479.fd-cmd-nav { display: flex; align-items: center; gap: 2px; margin: 0 0 0 10px; padding: 0; }480.fd-cmd-nav a { position: relative; display: block; margin: 0; padding: 8px 10px; border-radius: 7px; font: 500 0.875rem/1 ${sans}; color: var(--fd-cmd-soft); text-decoration: none; transition: color var(--fd-dur-1) linear, background var(--fd-dur-1) linear; }481.fd-cmd-nav a:hover { color: var(--fd-cmd-ink); background: #f0f0ec; }482.fd-cmd-nav a[aria-current="page"] { color: var(--fd-cmd-ink); }483.fd-cmd-nav a[aria-current="page"]::after { content: ""; position: absolute; left: 10px; right: 10px; bottom: -12px; height: 2px; background: var(--fd-cmd-ink); }484.fd-cmd-search { display: flex; align-items: center; gap: 9px; width: 300px; height: 36px; margin: 0 0 0 auto; padding: 0 6px 0 11px; border: 1px solid var(--fd-cmd-line); border-radius: 9px; background: #fff; font: 400 0.875rem/1 ${sans}; color: var(--fd-cmd-soft); text-align: left; cursor: text; appearance: none; -webkit-appearance: none; box-shadow: 0 1px 0 rgba(20,22,26,0.03); transition: border-color var(--fd-dur-1) linear, box-shadow var(--fd-dur-2) var(--fd-ease-out); }485.fd-cmd-search svg { display: block; flex: none; }486.fd-cmd-search:hover { border-color: #cfcfc8; box-shadow: 0 2px 8px rgba(20,22,26,0.06); }487.fd-cmd-ph { flex: 1; }488.fd-cmd-invite { display: inline-flex; align-items: center; margin: 0; padding: 9px 12px; border-radius: 8px; font: 500 0.8125rem/1 ${sans}; color: #fff; background: var(--fd-cmd-ink); text-decoration: none; }489.fd-cmd-invite:hover { background: #2a2d33; }490.fd-cmd-av { display: block; width: 32px; height: 32px; border-radius: 9999px; object-fit: cover; background: #e6e6e1; }491.fd-cmd-burger { display: none; place-items: center; width: 38px; height: 38px; margin: 0; padding: 0; border: 1px solid var(--fd-cmd-line); border-radius: 9px; background: #fff; color: var(--fd-cmd-ink); cursor: pointer; appearance: none; -webkit-appearance: none; }492.fd-cmd-burger svg { display: block; }493 494.fd-cmd-menu { position: absolute; left: 12px; right: 12px; top: 64px; margin: 0; padding: 10px; border: 1px solid var(--fd-cmd-line); border-radius: 14px; background: #fff; box-shadow: 0 18px 40px rgba(20,22,26,0.14); animation: fd-cmd-pop var(--fd-dur-2) var(--fd-ease-out); transform-origin: top right; }495.fd-cmd-menu[hidden] { display: none; }496.fd-cmd-msearch { display: flex; align-items: center; gap: 9px; width: 100%; margin: 0 0 6px; padding: 12px; border: 1px solid var(--fd-cmd-line); border-radius: 10px; background: var(--fd-cmd-paper); font: 400 0.9375rem/1 ${sans}; color: var(--fd-cmd-soft); text-align: left; cursor: text; appearance: none; -webkit-appearance: none; }497.fd-cmd-msearch svg { display: block; }498.fd-cmd-menu nav { display: block; margin: 0; padding: 0; }499.fd-cmd-menu nav a { display: block; margin: 0; padding: 13px 12px; border-radius: 9px; font: 500 1.0625rem/1 ${sans}; color: var(--fd-cmd-ink); text-decoration: none; }500.fd-cmd-menu nav a[aria-current="page"] { background: #f0f0ec; }501.fd-cmd-mfoot { display: flex; align-items: center; gap: 10px; margin: 6px 0 0; padding: 12px 4px 2px; border-top: 1px solid var(--fd-cmd-line); }502.fd-cmd-mfoot .fd-cmd-invite { margin-left: auto; }503 504.fd-cmd-layer { position: fixed; inset: 0; z-index: 80; display: flex; justify-content: center; align-items: flex-start; padding: 12vh 16px 16px; }505.fd-cmd-scrim { position: absolute; inset: 0; background: rgba(20,22,26,0.32); animation: fd-cmd-fade var(--fd-dur-2) linear; }506@keyframes fd-cmd-fade { from { opacity: 0; } }507@keyframes fd-cmd-pop { from { opacity: 0; transform: scale(0.97) translateY(-4px); } }508.fd-cmd-pal { position: relative; display: flex; flex-direction: column; width: min(760px, 100%); max-height: min(560px, 76vh); margin: 0; padding: 0; border: 1px solid #dcdcd6; border-radius: 16px; background: #fff; box-shadow: 0 1px 0 rgba(255,255,255,0.8) inset, 0 30px 80px -10px rgba(20,22,26,0.35), 0 8px 20px rgba(20,22,26,0.08); overflow: hidden; animation: fd-cmd-pop var(--fd-dur-2) var(--fd-ease-out); }509.fd-cmd-field { flex: none; display: flex; align-items: center; gap: 12px; margin: 0; padding: 0 14px 0 18px; height: 58px; border-bottom: 1px solid var(--fd-cmd-line); color: var(--fd-cmd-soft); }510.fd-cmd-field svg { display: block; flex: none; }511.fd-cmd-field input { flex: 1; min-width: 0; height: 100%; margin: 0; padding: 0; border: 0; outline: 0; background: transparent; font: 400 1.0625rem/1.2 ${sans}; letter-spacing: -0.01em; color: var(--fd-cmd-ink); appearance: none; -webkit-appearance: none; }512.fd-cmd-field input::placeholder { color: #9a9ca2; }513.fd-cmd-esc { margin: 0; padding: 5px 7px; border: 1px solid var(--fd-cmd-line); border-radius: 6px; background: var(--fd-cmd-paper); font: 500 0.6875rem/1 ${mono}; color: var(--fd-cmd-soft); cursor: pointer; appearance: none; -webkit-appearance: none; }514.fd-cmd-body { display: grid; grid-template-columns: minmax(0, 1fr) 248px; min-height: 0; flex: 1; }515.fd-cmd-list { position: relative; overflow-y: auto; margin: 0; padding: 6px 8px 10px; overscroll-behavior: contain; }516.fd-cmd-group { margin: 0; padding: 0; }517.fd-cmd-gh { position: sticky; top: -6px; z-index: 1; margin: 0; padding: 12px 10px 6px; background: #fff; font: 500 0.6875rem/1 ${mono}; letter-spacing: 0.06em; text-transform: uppercase; color: #8d9096; }518.fd-cmd-opt { position: relative; display: flex; align-items: center; gap: 11px; margin: 0; padding: 9px 10px; border-radius: 9px; color: var(--fd-cmd-ink); cursor: pointer; }519.fd-cmd-opt[aria-selected="true"] { background: #f1f2ff; }520.fd-cmd-opt[aria-selected="true"]::before { content: ""; position: absolute; left: -8px; top: 9px; bottom: 9px; width: 3px; border-radius: 0 3px 3px 0; background: var(--fd-cmd-accent); }521.fd-cmd-ic { display: grid; place-items: center; flex: none; width: 28px; height: 28px; border: 1px solid var(--fd-cmd-line); border-radius: 7px; background: var(--fd-cmd-paper); color: var(--fd-cmd-soft); }522.fd-cmd-ic svg { display: block; }523.fd-cmd-opt[aria-selected="true"] .fd-cmd-ic { color: var(--fd-cmd-accent); border-color: #cdd3ff; background: #fff; }524.fd-cmd-ini { font: 600 0.6875rem/1 ${sans}; color: var(--fd-cmd-ink); }525.fd-cmd-lab { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 500 0.9375rem/1.2 ${sans}; color: var(--fd-cmd-ink); }526.fd-cmd-mono { font: 500 0.875rem/1.2 ${mono}; }527.fd-cmd-lab mark { background: none; color: var(--fd-cmd-accent); font-weight: 700; text-decoration: underline; text-decoration-thickness: 1.5px; text-underline-offset: 3px; }528.fd-cmd-hint { flex: none; font: 400 0.8125rem/1 ${sans}; color: #8d9096; white-space: nowrap; }529.fd-cmd-keys { display: inline-flex; gap: 3px; flex: none; }530.fd-cmd-enter { display: block; flex: none; color: var(--fd-cmd-accent); opacity: 0; transform: translateX(-4px); transition: opacity var(--fd-dur-1) linear, transform var(--fd-dur-2) var(--fd-ease-out); }531.fd-cmd-opt[aria-selected="true"] .fd-cmd-enter { opacity: 1; transform: none; }532.fd-cmd-empty { margin: 0; padding: 40px 16px; text-align: center; }533.fd-cmd-empty p { margin: 0 0 6px; font: 600 0.9375rem/1.3 ${sans}; color: var(--fd-cmd-ink); }534.fd-cmd-empty span { font: 400 0.8125rem/1.4 ${sans}; color: var(--fd-cmd-soft); }535.fd-cmd-empty code { font: 500 0.8125rem/1 ${mono}; color: var(--fd-cmd-ink); }536.fd-cmd-prev { margin: 0; padding: 18px 18px; border-left: 1px solid var(--fd-cmd-line); background: var(--fd-cmd-paper); overflow: hidden; }537.fd-cmd-pk { display: block; margin: 0 0 6px; font: 500 0.6875rem/1 ${mono}; letter-spacing: 0.06em; text-transform: uppercase; color: var(--fd-cmd-accent); }538.fd-cmd-pt { margin: 0 0 16px; font: 600 1rem/1.3 ${sans}; letter-spacing: -0.01em; color: var(--fd-cmd-ink); word-break: break-word; }539.fd-cmd-spark { display: block; width: 100%; height: 92px; margin: 0 0 14px; border-bottom: 1px solid var(--fd-cmd-line); }540.fd-cmd-pstats { display: grid; gap: 6px; font: 400 0.8125rem/1.3 ${sans}; color: var(--fd-cmd-soft); }541.fd-cmd-pstats b { font: 600 0.875rem/1 ${mono}; color: var(--fd-cmd-ink); }542.fd-cmd-pd { margin: 0; font: 400 0.8125rem/1.5 ${sans}; color: var(--fd-cmd-soft); }543.fd-cmd-foot { flex: none; display: flex; align-items: center; gap: 16px; margin: 0; padding: 10px 16px; border-top: 1px solid var(--fd-cmd-line); background: var(--fd-cmd-paper); font: 400 0.75rem/1 ${sans}; color: var(--fd-cmd-soft); }544.fd-cmd-foot span { display: inline-flex; align-items: center; gap: 4px; }545.fd-cmd-count { margin-left: auto; font-family: ${mono}; }546 547.fd-cmd-toast { position: fixed; left: 50%; bottom: 24px; z-index: 90; margin: 0; padding: 11px 16px; border-radius: 10px; background: var(--fd-cmd-ink); color: #fff; font: 500 0.875rem/1.2 ${sans}; transform: translateX(-50%); box-shadow: 0 12px 30px rgba(20,22,26,0.3); animation: fd-cmd-toast var(--fd-dur-3) var(--fd-spring); }548@keyframes fd-cmd-toast { from { opacity: 0; transform: translate(-50%, 12px) scale(0.96); } }549 550@media (max-width: 1180px) { .fd-cmd-search { width: 220px; } .fd-cmd-crumb .fd-cmd-env { display: none; } }551@media (max-width: 980px) {552 .fd-cmd-nav, .fd-cmd-crumb, .fd-cmd-bar > .fd-cmd-invite { display: none; }553 .fd-cmd-burger { display: grid; }554 .fd-cmd-search { width: auto; flex: 1; max-width: 360px; }555}556@media (max-width: 600px) {557 .fd-cmd-bar { padding: 0 14px; gap: 10px; }558 .fd-cmd-search { width: 38px; flex: none; justify-content: center; padding: 0; }559 .fd-cmd-search .fd-cmd-ph, .fd-cmd-search kbd { display: none; }560 .fd-cmd-layer { padding: 8px; }561 .fd-cmd-pal { max-height: calc(100vh - 16px); max-height: calc(100dvh - 16px); }562 .fd-cmd-body { grid-template-columns: 1fr; }563 .fd-cmd-prev, .fd-cmd-hint, .fd-cmd-foot span:nth-child(-n+2) { display: none; }564}565@media (prefers-reduced-motion: reduce) {566 .fd-cmd-pal, .fd-cmd-menu, .fd-cmd-toast, .fd-cmd-scrim { animation: none; }567 .fd-cmd-enter { transition: none; }568}569`;570 571/* ── Demo ─────────────────────────────────────────────────────────────── */572 573const STEPS = [574 { name: "Viewed bike", v: 48210, pct: 100 },575 { name: "Selected plan", v: 21904, pct: 45.4 },576 { name: "Entered card", v: 12377, pct: 25.7 },577 { name: "payment_failed", v: 312, pct: 0.6, bad: true },578 { name: "bike_unlocked", v: 11208, pct: 23.2 },579];580 581export function Demo() {582 return (583 <div className="fd-cmd-demo">584 <style>{demoCss}</style>585 <CommandNav />586 <main>587 <div className="fd-cmd-demo-head">588 <div>589 <p className="fd-cmd-demo-k">Funnels / Checkout funnel</p>590 <h1>Checkout funnel</h1>591 </div>592 <p className="fd-cmd-demo-tip">Press <kbd>⌘</kbd><kbd>K</kbd> or <kbd>/</kbd> to jump anywhere</p>593 </div>594 <div className="fd-cmd-demo-kpis">595 <div><span>Conversion</span><b>23.2%</b><em>+1.8 pts</em></div>596 <div><span>Median time to unlock</span><b>2m 41s</b><em>−14s</em></div>597 <div><span>Failed payments</span><b>312</b><em data-bad="">+41</em></div>598 <div><span>Revenue, 30 days</span><b>€184,920</b><em>+6.2%</em></div>599 </div>600 <section className="fd-cmd-demo-funnel" aria-label="Funnel steps">601 {STEPS.map((s, i) => (602 <div key={s.name} className="fd-cmd-demo-step">603 <span className="fd-cmd-demo-n">{i + 1}</span>604 <span className="fd-cmd-demo-name">{s.name}</span>605 <span className="fd-cmd-demo-track"><span style={{ width: `${Math.max(s.pct, 1.2)}%` }} data-bad={s.bad ? "" : undefined} /></span>606 <span className="fd-cmd-demo-v">{s.v.toLocaleString("en-US")}</span>607 <span className="fd-cmd-demo-p">{s.pct}%</span>608 </div>609 ))}610 </section>611 <p className="fd-cmd-demo-note">Last 30 days · 48,210 sessions · Copenhagen, Malmö and Oslo stations · updated 4 minutes ago</p>612 </main>613 </div>614 );615}616 617const demoCss = `618.fd-cmd-demo { margin: 0; padding: 0; min-height: 140vh; background: #fbfbf9; color: #14161a; font: 400 14px/1.5 ${sans}; }619.fd-cmd-demo *, .fd-cmd-demo *::before, .fd-cmd-demo *::after { box-sizing: border-box; }620.fd-cmd-demo main { display: block; max-width: 1200px; margin: 0 auto; padding: 36px 20px 120px; }621.fd-cmd-demo-head { display: flex; justify-content: space-between; align-items: flex-end; gap: 20px; margin: 0 0 28px; }622.fd-cmd-demo-k { margin: 0 0 8px; font: 500 0.75rem/1 ${mono}; color: #6a6d74; }623.fd-cmd-demo h1 { margin: 0; font: 650 2.25rem/1.05 ${sans}; letter-spacing: -0.035em; }624.fd-cmd-demo-tip { margin: 0; font: 400 0.8125rem/1 ${sans}; color: #6a6d74; display: flex; align-items: center; gap: 4px; }625.fd-cmd-demo kbd { display: inline-grid; place-items: center; min-width: 20px; height: 20px; padding: 0 5px; border: 1px solid #e6e6e1; border-bottom-width: 2px; border-radius: 5px; background: #fff; font: 500 0.6875rem/1 ${mono}; color: #14161a; }626.fd-cmd-demo-kpis { display: grid; grid-template-columns: repeat(4, 1fr); margin: 0 0 28px; border: 1px solid #e6e6e1; border-radius: 14px; background: #fff; }627.fd-cmd-demo-kpis div { display: grid; gap: 6px; padding: 18px 20px; border-left: 1px solid #e6e6e1; }628.fd-cmd-demo-kpis div:first-child { border-left: 0; }629.fd-cmd-demo-kpis span { font: 400 0.8125rem/1 ${sans}; color: #6a6d74; }630.fd-cmd-demo-kpis b { font: 600 1.625rem/1 ${sans}; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; }631.fd-cmd-demo-kpis em { font: 500 0.75rem/1 ${mono}; font-style: normal; color: #1d6b3a; }632.fd-cmd-demo-kpis em[data-bad] { color: #c2410c; }633.fd-cmd-demo-funnel { margin: 0; padding: 8px 20px; border: 1px solid #e6e6e1; border-radius: 14px; background: #fff; }634.fd-cmd-demo-step { display: grid; grid-template-columns: 24px 160px 1fr 80px 56px; align-items: center; gap: 14px; padding: 14px 0; border-top: 1px solid #f0f0ec; }635.fd-cmd-demo-step:first-child { border-top: 0; }636.fd-cmd-demo-n { font: 500 0.75rem/1 ${mono}; color: #8d9096; }637.fd-cmd-demo-name { font: 500 0.875rem/1.2 ${mono}; }638.fd-cmd-demo-track { display: block; height: 28px; border-radius: 6px; background: #f4f4f1; overflow: hidden; }639.fd-cmd-demo-track span { display: block; height: 100%; border-radius: 6px; background: #2a44ff; }640.fd-cmd-demo-track span[data-bad] { background: #ea580c; }641.fd-cmd-demo-v, .fd-cmd-demo-p { font: 500 0.875rem/1 ${mono}; text-align: right; font-variant-numeric: tabular-nums; }642.fd-cmd-demo-p { color: #6a6d74; }643.fd-cmd-demo-note { margin: 14px 0 0; font: 400 0.8125rem/1.4 ${sans}; color: #6a6d74; }644@media (max-width: 760px) {645 .fd-cmd-demo main { padding: 24px 14px 100px; }646 .fd-cmd-demo-tip { display: none; }647 .fd-cmd-demo-kpis { grid-template-columns: 1fr 1fr; }648 .fd-cmd-demo-kpis div:nth-child(3) { border-left: 0; }649 .fd-cmd-demo-kpis div:nth-child(n+3) { border-top: 1px solid #e6e6e1; }650 .fd-cmd-demo-step { grid-template-columns: 1fr 64px 48px; gap: 8px 10px; }651 .fd-cmd-demo-n { display: none; }652 .fd-cmd-demo-track { grid-column: 1 / -1; grid-row: 2; height: 18px; }653}654`;More Navbars. Same file contract.
Questions about Command Nav
Run npx shadcn@latest add https://freedesignmd.com/r/command-nav.json, or open the page and copy the file into your project. It has zero dependencies besides React. What you copy is the exact file that renders the live demo.
Navbar with command palette: search opens a real ⌘K palette with recents, grouped fuzzy results and a live preview. It is a free, MIT-licensed React navbars component from the freedesignmd library.
Yes. It is free and MIT-licensed, use, modify and ship it in personal and commercial projects without attribution.
All evergreen browsers.
Yes. It is a plain React component with its own styles, so it runs in Next.js, Vite, Remix and Lovable projects, and it needs no Tailwind setup. It has zero dependencies besides React.


