Search Command

A ⌘K command palette with fuzzy matching, sections, recent items and complete keyboard navigation.

  • Forms
  • Logistics
  • React only · 0 deps
  • 1 file
Full screen

Browser support · All evergreen browsers. Backdrop blur falls back to an opaque panel with prefers-reduced-transparency.

Install. One command, or copy the file.

$npx shadcn@latest add https://freedesignmd.com/r/search-command.json
  • Byte-identical to the preview
  • No Tailwind or config needed
  • SSR-safe · keyboard accessible
  • Respects reduced motion & transparency
components/forms/search-command.tsx
1/**
2 * SearchCommand — a ⌘K command palette: fuzzy filtering with highlighted
3 * matches, grouped sections, recent items, full keyboard navigation and a
4 * footer with key hints.
5 *
6 * Props: open, onOpenChange, sections[] ({ id, heading, items[] }), recent
7 * (item ids shown first when the query is empty), onSelect(item),
8 * placeholder, hotkey (default true: ⌘K / Ctrl+K toggles), theme.
9 * Item = { id, label, hint?, icon?, keywords?, shortcut? }.
10 * Exports: SearchCommand, fuzzyScore.
11 *
12 * Render it near the root of your app: it is position: fixed and does not use
13 * a portal (so it works without a DOM target and in SSR).
14 * Browser support: all evergreen browsers.
15 * Accessibility: role="dialog" aria-modal, the input is a combobox
16 * (aria-expanded, aria-controls, aria-activedescendant) over a role="listbox";
17 * focus is trapped, Escape closes and returns focus to the opener, results
18 * count is announced politely. Keys: ↑/↓, Home/End, Enter, Esc, ⌘K.
19 * Reduced motion: opens with a fade only.
20 */
21import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
22import type { ReactNode } from "react";
23
24export interface CommandItem {
25 id: string;
26 label: string;
27 hint?: string;
28 icon?: ReactNode;
29 keywords?: string;
30 shortcut?: string;
31}
32export interface CommandSection {
33 id: string;
34 heading: string;
35 items: CommandItem[];
36}
37
38/** Subsequence fuzzy score with bonuses for word starts and runs. Returns null when it does not match. */
39export function fuzzyScore(query: string, text: string): { score: number; idx: number[] } | null {
40 const q = query.toLowerCase().replace(/\s+/g, "");
41 const t = text.toLowerCase();
42 if (!q) return { score: 0, idx: [] };
43 // exact substring wins
44 const sub = t.indexOf(query.toLowerCase().trim());
45 if (sub >= 0) return { score: 100 - sub + query.length, idx: Array.from({ length: query.trim().length }, (_, i) => sub + i) };
46 let ti = 0;
47 let score = 0;
48 let run = 0;
49 const idx: number[] = [];
50 for (const ch of q) {
51 const f = t.indexOf(ch, ti);
52 if (f < 0) return null;
53 const start = f === 0 || /[\s\-_/.#]/.test(t[f - 1]);
54 run = f === ti && idx.length ? run + 1 : 0;
55 score += 1 + (start ? 6 : 0) + run * 3 - Math.min(f - ti, 6) * 0.2;
56 idx.push(f);
57 ti = f + 1;
58 }
59 return { score, idx };
60}
61
62function Hl({ text, idx }: { text: string; idx: number[] }) {
63 if (!idx.length) return <>{text}</>;
64 const set = new Set(idx);
65 const out: ReactNode[] = [];
66 let buf = "";
67 let on = false;
68 for (let i = 0; i < text.length; i++) {
69 const m = set.has(i);
70 if (m !== on && buf) {
71 out.push(on ? <mark key={i}>{buf}</mark> : buf);
72 buf = "";
73 }
74 on = m;
75 buf += text[i];
76 }
77 if (buf) out.push(on ? <mark key="end">{buf}</mark> : buf);
78 return <>{out}</>;
79}
80
81export interface SearchCommandProps {
82 open: boolean;
83 onOpenChange: (o: boolean) => void;
84 sections: CommandSection[];
85 recent?: string[];
86 onSelect?: (item: CommandItem, section: CommandSection) => void;
87 placeholder?: string;
88 hotkey?: boolean;
89 theme?: "light" | "dark";
90 className?: string;
91}
92
93export function SearchCommand({ open, onOpenChange, sections, recent = [], onSelect, placeholder = "Search or run a command…", hotkey = true, theme, className }: SearchCommandProps) {
94 const uid = useId();
95 const [q, setQ] = useState("");
96 const [cur, setCur] = useState(0);
97 const input = useRef<HTMLInputElement>(null);
98 const list = useRef<HTMLDivElement>(null);
99 const opener = useRef<Element | null>(null);
100 const [mouse, setMouse] = useState(false);
101
102 useEffect(() => {
103 if (!hotkey) return;
104 const on = (e: KeyboardEvent) => {
105 if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
106 e.preventDefault();
107 onOpenChange(!open);
108 }
109 };
110 window.addEventListener("keydown", on);
111 return () => window.removeEventListener("keydown", on);
112 }, [hotkey, open, onOpenChange]);
113
114 useEffect(() => {
115 if (open) {
116 opener.current = document.activeElement;
117 setQ("");
118 setCur(0);
119 const t = window.setTimeout(() => input.current?.focus(), 20);
120 return () => window.clearTimeout(t);
121 }
122 (opener.current as HTMLElement | null)?.focus?.();
123 }, [open]);
124
125 const groups = useMemo(() => {
126 const all = sections.flatMap((s) => s.items.map((i) => ({ i, s })));
127 if (!q.trim()) {
128 const rec = recent.map((id) => all.find((x) => x.i.id === id)).filter(Boolean) as typeof all;
129 const out: { section: CommandSection; heading: string; rows: { item: CommandItem; idx: number[] }[] }[] = [];
130 if (rec.length) out.push({ section: rec[0].s, heading: "Recent", rows: rec.map((x) => ({ item: x.i, idx: [] })) });
131 sections.forEach((s) => s.items.some((i) => !recent.includes(i.id)) && out.push({ section: s, heading: s.heading, rows: s.items.filter((i) => !recent.includes(i.id)).slice(0, 4).map((i) => ({ item: i, idx: [] })) }));
132 return out;
133 }
134 return sections
135 .map((s) => {
136 const rows = s.items
137 .map((item) => {
138 const a = fuzzyScore(q, item.label);
139 const b = a ? null : fuzzyScore(q, `${item.keywords ?? ""} ${item.hint ?? ""}`);
140 if (!a && !b) return null;
141 return { item, idx: a?.idx ?? [], score: a ? a.score + 20 : (b?.score ?? 0) };
142 })
143 .filter(Boolean) as { item: CommandItem; idx: number[]; score: number }[];
144 rows.sort((x, y) => y.score - x.score);
145 return { section: s, heading: s.heading, rows, top: rows[0]?.score ?? -1 };
146 })
147 .filter((g) => g.rows.length)
148 .sort((a, b) => (b as { top: number }).top - (a as { top: number }).top);
149 }, [q, sections, recent]);
150
151 const flat = groups.flatMap((g) => g.rows.map((r) => ({ ...r, section: g.section })));
152 useEffect(() => setCur(0), [q]);
153 useEffect(() => {
154 list.current?.querySelector<HTMLElement>(`[data-i="${cur}"]`)?.scrollIntoView({ block: "nearest" });
155 }, [cur]);
156
157 const run = useCallback(
158 (n: number) => {
159 const r = flat[n];
160 if (!r) return;
161 onSelect?.(r.item, r.section);
162 onOpenChange(false);
163 },
164 [flat, onSelect, onOpenChange],
165 );
166
167 const onKey = (e: React.KeyboardEvent) => {
168 setMouse(false);
169 if (e.key === "ArrowDown") (e.preventDefault(), setCur((c) => (c + 1) % Math.max(flat.length, 1)));
170 else if (e.key === "ArrowUp") (e.preventDefault(), setCur((c) => (c - 1 + flat.length) % Math.max(flat.length, 1)));
171 else if (e.key === "Home") (e.preventDefault(), setCur(0));
172 else if (e.key === "End") (e.preventDefault(), setCur(flat.length - 1));
173 else if (e.key === "Enter") (e.preventDefault(), run(cur));
174 else if (e.key === "Escape") (e.preventDefault(), onOpenChange(false));
175 else if (e.key === "Tab") e.preventDefault(); // keep focus in the palette
176 };
177
178 if (!open) return <style>{css}</style>;
179
180 let n = -1;
181 return (
182 <div className={`fd-cmd${className ? " " + className : ""}`} data-fd-theme={theme}>
183 <style>{css}</style>
184 <div className="fd-cmd-scrim" onMouseDown={() => onOpenChange(false)} />
185 <div className="fd-cmd-panel" role="dialog" aria-modal="true" aria-label="Command palette" onKeyDown={onKey}>
186 <div className="fd-cmd-top">
187 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5" /></svg>
188 <input
189 ref={input}
190 role="combobox"
191 aria-expanded="true"
192 aria-controls={`${uid}-list`}
193 aria-activedescendant={flat.length ? `${uid}-o${cur}` : undefined}
194 aria-autocomplete="list"
195 autoComplete="off"
196 spellCheck={false}
197 placeholder={placeholder}
198 value={q}
199 onChange={(e) => setQ(e.target.value)}
200 />
201 {q && <button type="button" className="fd-cmd-clear" aria-label="Clear search" onClick={() => { setQ(""); input.current?.focus(); }}>Clear</button>}
202 <kbd>esc</kbd>
203 </div>
204
205 <div ref={list} id={`${uid}-list`} className="fd-cmd-list" role="listbox" aria-label="Results" onMouseMove={() => !mouse && setMouse(true)}>
206 {flat.length === 0 ? (
207 <div className="fd-cmd-empty">
208 <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5M8.5 11h5" /></svg>
209 <p>No results for “{q}”</p>
210 <span>Try a shipment ID, warehouse code or action.</span>
211 </div>
212 ) : (
213 groups.map((g) => (
214 <div key={g.heading + g.section.id} role="group" aria-label={g.heading} className="fd-cmd-group">
215 <div className="fd-cmd-head" aria-hidden="true">{g.heading}</div>
216 {g.rows.map((r) => {
217 n++;
218 const i = n;
219 return (
220 <div
221 key={r.item.id + g.heading}
222 id={`${uid}-o${i}`}
223 role="option"
224 aria-selected={i === cur}
225 data-i={i}
226 className="fd-cmd-item"
227 data-active={i === cur || undefined}
228 onMouseMove={() => mouse && setCur(i)}
229 onClick={() => run(i)}
230 >
231 <span className="fd-cmd-ic">{r.item.icon}</span>
232 <span className="fd-cmd-txt">
233 <span className="fd-cmd-label"><Hl text={r.item.label} idx={r.idx} /></span>
234 {r.item.hint && <span className="fd-cmd-hint">{r.item.hint}</span>}
235 </span>
236 {r.item.shortcut && <kbd>{r.item.shortcut}</kbd>}
237 <svg className="fd-cmd-enter" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M20 5v7a3 3 0 0 1-3 3H5M9 11l-4 4 4 4" /></svg>
238 </div>
239 );
240 })}
241 </div>
242 ))
243 )}
244 </div>
245
246 <div className="fd-cmd-foot" aria-hidden="true">
247 <span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
248 <span><kbd>↵</kbd> select</span>
249 <span><kbd>esc</kbd> close</span>
250 <b>{flat.length} {flat.length === 1 ? "result" : "results"}</b>
251 </div>
252 <span className="fd-cmd-sr" role="status" aria-live="polite">{flat.length} results</span>
253 </div>
254 </div>
255 );
256}
257
258const css = `
259:where(.fd-cmd,.fd-cmd *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
260:where(.fd-cmd,.fd-cmd *)::before,:where(.fd-cmd,.fd-cmd *)::after{box-sizing:border-box}
261:where(.fd-cmd svg){display:block;flex-shrink:0}
262:where(.fd-cmd button,.fd-cmd input){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0;outline:0}
263.fd-cmd{
264 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-spring:cubic-bezier(.34,1.56,.64,1);--fd-dur-1:120ms;--fd-dur-2:240ms;--fd-dur-3:420ms;
265 --c-bg:rgba(255,255,255,.92);--c-solid:#fff;--c-fg:#0B0B0F;--c-mut:#6B6B76;--c-line:rgba(11,11,15,.08);--c-hover:rgba(11,11,15,.05);--c-active:rgba(0,122,255,.1);--c-accent:#007AFF;--c-mark:#0058C7;--c-kbd:rgba(11,11,15,.06);
266 position:fixed;inset:0;z-index:1000;display:flex;align-items:flex-start;justify-content:center;padding:min(14vh,120px) 16px 16px;
267 font:400 15px/1.4 ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;letter-spacing:-.005em;color:var(--c-fg);
268}
269[data-fd-theme="dark"].fd-cmd,.fd-cmd[data-fd-theme="dark"]{--c-bg:rgba(24,24,30,.86);--c-solid:#18181E;--c-fg:#F5F5F7;--c-mut:#9A9AA6;--c-line:rgba(255,255,255,.09);--c-hover:rgba(255,255,255,.06);--c-active:rgba(10,132,255,.22);--c-accent:#0A84FF;--c-mark:#6CB4FF;--c-kbd:rgba(255,255,255,.1)}
270.fd-cmd-scrim{position:absolute;inset:0;background:rgba(11,11,15,.35);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);animation:fd-cmd-fade var(--fd-dur-2) var(--fd-ease-out)}
271.fd-cmd-panel{position:relative;display:flex;flex-direction:column;width:100%;max-width:640px;max-height:min(560px,calc(100vh - 140px));border-radius:20px;overflow:hidden;
272 background:var(--c-bg);-webkit-backdrop-filter:blur(28px) saturate(1.6);backdrop-filter:blur(28px) saturate(1.6);
273 box-shadow:0 0 0 1px var(--c-line),0 1px 0 rgba(255,255,255,.1) inset,0 4px 8px rgba(0,0,0,.06),0 40px 80px -20px rgba(0,0,0,.45);animation:fd-cmd-in var(--fd-dur-3) var(--fd-spring)}
274.fd-cmd-top{flex:none;display:flex;align-items:center;gap:12px;padding:0 16px;height:60px;border-bottom:1px solid var(--c-line);color:var(--c-mut)}
275.fd-cmd-top input{flex:1;min-width:0;height:100%;color:var(--c-fg);font-family:inherit;font-size:17px;letter-spacing:-.01em;caret-color:var(--c-accent)}
276.fd-cmd-top input::placeholder{color:var(--c-mut);opacity:.8}
277.fd-cmd-clear{cursor:pointer;height:24px;padding:0 8px;border-radius:6px;font-family:inherit;font-size:12px;font-weight:500;color:var(--c-mut)}.fd-cmd-clear:hover{background:var(--c-hover);color:var(--c-fg)}
278.fd-cmd-clear:focus-visible{outline:2px solid var(--c-accent);outline-offset:1px}
279.fd-cmd kbd{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 6px;border-radius:6px;background:var(--c-kbd);color:var(--c-mut);font:500 11px/1 ui-monospace,"SF Mono","JetBrains Mono",Menlo,monospace}
280.fd-cmd-list{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;padding:8px;scroll-padding:8px}
281.fd-cmd-group+.fd-cmd-group{margin-top:6px}
282.fd-cmd-head{padding:10px 12px 6px;font-size:12px;font-weight:600;line-height:1;letter-spacing:.02em;color:var(--c-mut)}
283.fd-cmd-item{display:flex;align-items:center;gap:12px;min-height:48px;padding:8px 12px;border-radius:12px;cursor:pointer;transition:background-color var(--fd-dur-1) var(--fd-ease-out)}
284.fd-cmd-item[data-active]{background:var(--c-active)}
285.fd-cmd-ic{display:grid;place-items:center;width:32px;height:32px;border-radius:9px;background:var(--c-hover);color:var(--c-mut);transition:background-color var(--fd-dur-1),color var(--fd-dur-1)}
286.fd-cmd-item[data-active] .fd-cmd-ic{background:var(--c-accent);color:#fff}
287.fd-cmd-ic svg{width:16px;height:16px}
288.fd-cmd-txt{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0}
289.fd-cmd-label{font-size:15px;font-weight:500;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
290.fd-cmd-hint{font-size:12.5px;line-height:1.3;color:var(--c-mut);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
291.fd-cmd mark{background:none;color:var(--c-mark);font-weight:700}
292.fd-cmd-enter{opacity:0;color:var(--c-mut);transform:translateX(-4px);transition:opacity var(--fd-dur-1),transform var(--fd-dur-2) var(--fd-ease-out)}
293.fd-cmd-item[data-active] .fd-cmd-enter{opacity:1;transform:none}
294.fd-cmd-item[data-active] kbd{display:none}
295.fd-cmd-empty{display:flex;flex-direction:column;align-items:center;gap:6px;padding:48px 16px;text-align:center;color:var(--c-mut)}
296.fd-cmd-empty p{margin-top:8px;font-size:15px;font-weight:500;color:var(--c-fg)}.fd-cmd-empty span{font-size:13px}
297.fd-cmd-foot{flex:none;display:flex;align-items:center;gap:16px;padding:0 16px;height:44px;border-top:1px solid var(--c-line);font-size:12px;color:var(--c-mut)}
298.fd-cmd-foot span{display:inline-flex;align-items:center;gap:4px}.fd-cmd-foot kbd{min-width:20px;height:20px}
299.fd-cmd-foot b{margin-left:auto;font-weight:500;font-variant-numeric:tabular-nums}
300.fd-cmd-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
301@keyframes fd-cmd-fade{from{opacity:0}}
302@keyframes fd-cmd-in{from{opacity:0;transform:translateY(-12px) scale(.96)}}
303@media (max-width:560px){.fd-cmd{padding-top:12px}.fd-cmd-foot span:nth-child(n+2){display:none}.fd-cmd-panel{max-height:calc(100vh - 28px)}}
304@media (prefers-reduced-motion:reduce){.fd-cmd-panel{animation:fd-cmd-fade 100ms linear}.fd-cmd-scrim{animation:none}.fd-cmd-enter{transition:none}}
305@media (prefers-reduced-transparency:reduce),(prefers-contrast:more){.fd-cmd-panel{background:var(--c-solid);-webkit-backdrop-filter:none;backdrop-filter:none;box-shadow:0 0 0 1px currentColor}.fd-cmd-scrim{-webkit-backdrop-filter:none;backdrop-filter:none}}
306`;
307
308/* ───────────────────────────── demo ───────────────────────────── */
309
310const I = (d: string) => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d={d} /></svg>;
311const box = I("M21 8l-9-5-9 5v8l9 5 9-5zM3 8l9 5 9-5M12 13v8");
312const truck = I("M1 6h13v10H1zM14 10h5l3 3v3h-8M6 19a2 2 0 1 0 0-4 2 2 0 0 0 0 4zM18 19a2 2 0 1 0 0-4 2 2 0 0 0 0 4z");
313const pin = I("M12 21s7-6 7-12a7 7 0 1 0-14 0c0 6 7 12 7 12zM12 11.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z");
314const plus = I("M12 5v14M5 12h14");
315const alert = I("M12 3l10 18H2zM12 10v5M12 18h.01");
316const grid = I("M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z");
317const user = I("M20 21a8 8 0 0 0-16 0M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8z");
318const moon = I("M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5z");
319
320const SECTIONS: CommandSection[] = [
321 {
322 id: "ship",
323 heading: "Shipments",
324 items: [
325 { id: "s1", label: "SHP-48213 · Rotterdam → Chicago", hint: "In transit · ETA 3 Nov · 12 pallets", icon: box, keywords: "container ocean maersk" },
326 { id: "s2", label: "SHP-48190 · Shenzhen → Los Angeles", hint: "Customs hold · Port of LA", icon: alert, keywords: "delay hold customs" },
327 { id: "s3", label: "SHP-48177 · Hamburg → Singapore", hint: "Delivered 27 Oct · POD signed", icon: box, keywords: "delivered" },
328 { id: "s4", label: "SHP-48251 · Monterrey → Dallas", hint: "Out for delivery · Driver R. Alvarez", icon: truck, keywords: "truck road ftl" },
329 ],
330 },
331 {
332 id: "wh",
333 heading: "Warehouses",
334 items: [
335 { id: "w1", label: "DC-04 Memphis Hub", hint: "92% capacity · 38 docks", icon: pin, keywords: "tennessee distribution center" },
336 { id: "w2", label: "DC-11 Rotterdam Maasvlakte", hint: "71% capacity · 24 docks", icon: pin, keywords: "netherlands" },
337 { id: "w3", label: "DC-07 Reno Crossdock", hint: "58% capacity · 16 docks", icon: pin, keywords: "nevada" },
338 ],
339 },
340 {
341 id: "act",
342 heading: "Actions",
343 items: [
344 { id: "a1", label: "Create shipment", hint: "Start a new booking", icon: plus, shortcut: "C" },
345 { id: "a2", label: "Reassign carrier", hint: "Move selected loads to another carrier", icon: truck, shortcut: "R" },
346 { id: "a3", label: "Escalate to customs broker", hint: "Open a case for held shipments", icon: alert },
347 { id: "a4", label: "Toggle dark mode", hint: "Appearance", icon: moon, shortcut: "⌘ D" },
348 ],
349 },
350 {
351 id: "nav",
352 heading: "Go to",
353 items: [
354 { id: "n1", label: "Dashboard", icon: grid, shortcut: "G D" },
355 { id: "n2", label: "Carriers", icon: truck, shortcut: "G C" },
356 { id: "n3", label: "Team settings", icon: user, shortcut: "G S" },
357 ],
358 },
359];
360
361const ROWS = [
362 ["SHP-48213", "Rotterdam → Chicago", "In transit", "3 Nov", "ok"],
363 ["SHP-48190", "Shenzhen → Los Angeles", "Customs hold", "—", "warn"],
364 ["SHP-48251", "Monterrey → Dallas", "Out for delivery", "Today", "ok"],
365 ["SHP-48177", "Hamburg → Singapore", "Delivered", "27 Oct", "done"],
366 ["SHP-48302", "Antwerp → Newark", "Booked", "9 Nov", "idle"],
367];
368
369export function Demo() {
370 const [theme, setTheme] = useState<"light" | "dark">("light");
371 const [open, setOpen] = useState(false);
372 const [recent, setRecent] = useState<string[]>(["s2", "a1", "w1"]);
373 const [log, setLog] = useState("");
374 const isMac = useMemo(() => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform), []);
375 const [mac, setMac] = useState(true);
376 useEffect(() => setMac(isMac), [isMac]);
377
378 return (
379 <div className="fd-cmd-demo" data-fd-theme={theme}>
380 <style>{demoCss}</style>
381 <aside className="fd-cmd-demo-side" aria-label="Navigation">
382 <div className="fd-cmd-demo-logo"><i aria-hidden="true" />Haulr Ops</div>
383 {["Dashboard", "Shipments", "Warehouses", "Carriers", "Reports"].map((n, i) => <a key={n} href={`#${n}`} aria-current={i === 0 ? "page" : undefined}>{n}</a>)}
384 </aside>
385 <main className="fd-cmd-demo-main">
386 <div className="fd-cmd-demo-top">
387 <button type="button" className="fd-cmd-demo-search" onClick={() => setOpen(true)} aria-haspopup="dialog">
388 <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="M20 20l-3.5-3.5" /></svg>
389 <span>Search shipments, sites, actions…</span>
390 <kbd>{mac ? "⌘" : "Ctrl"} K</kbd>
391 </button>
392 <button type="button" className="fd-cmd-demo-theme" aria-label={theme === "dark" ? "Switch to light theme" : "Switch to dark theme"} onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>{theme === "dark" ? "Light" : "Dark"}</button>
393 </div>
394 <h1>Operations overview</h1>
395 <section className="fd-cmd-demo-kpis" aria-label="Key metrics">
396 {[["On-time delivery", "94.2%", "+1.1 pt"], ["Active shipments", "1,284", "+86"], ["At risk", "17", "−4"], ["Dock utilisation", "81%", "+3 pt"]].map(([k, v, d]) => (
397 <div key={k}><small>{k}</small><b>{v}</b><span data-neg={d.startsWith("−") && k !== "At risk" ? "" : undefined}>{d} vs. last week</span></div>
398 ))}
399 </section>
400 <section className="fd-cmd-demo-table" aria-label="Recent shipments">
401 <div className="fd-cmd-demo-th"><span>Shipment</span><span>Lane</span><span>Status</span><span>ETA</span></div>
402 {ROWS.map(([id, lane, st, eta, k]) => (
403 <div key={id} className="fd-cmd-demo-tr"><b>{id}</b><span>{lane}</span><span data-k={k}><i />{st}</span><span>{eta}</span></div>
404 ))}
405 </section>
406 {log && <p className="fd-cmd-demo-log" role="status">Ran: {log}</p>}
407 </main>
408 <SearchCommand
409 open={open}
410 onOpenChange={setOpen}
411 sections={SECTIONS}
412 recent={recent}
413 theme={theme}
414 onSelect={(it) => {
415 setLog(it.label);
416 setRecent((r) => [it.id, ...r.filter((x) => x !== it.id)].slice(0, 4));
417 if (it.id === "a4") setTheme((t) => (t === "dark" ? "light" : "dark"));
418 }}
419 />
420 </div>
421 );
422}
423
424const demoCss = `
425:where(.fd-cmd-demo,.fd-cmd-demo *){box-sizing:border-box;margin:0;padding:0;font:inherit;letter-spacing:inherit;color:inherit;text-decoration:none;list-style:none}
426:where(.fd-cmd-demo,.fd-cmd-demo *)::before,:where(.fd-cmd-demo,.fd-cmd-demo *)::after{box-sizing:border-box}
427:where(.fd-cmd-demo button){appearance:none;-webkit-appearance:none;background:none;border:0;border-radius:0}
428.fd-cmd-demo{--d-font:ui-sans-serif,-apple-system,BlinkMacSystemFont,"SF Pro Text","Inter","Segoe UI",Roboto,sans-serif;--d-disp:ui-sans-serif,-apple-system,"SF Pro Display","Inter",sans-serif;
429 --fd-ease-out:cubic-bezier(.22,1,.36,1);--fd-dur-2:240ms;
430 --d-bg:#F5F6F8;--d-card:#fff;--d-ink:#0B0B0F;--d-mut:#6B6B76;--d-line:rgba(11,11,15,.08);--d-accent:#007AFF;--d-side:#fff;
431 display:grid;grid-template-columns:232px minmax(0,1fr);min-height:100vh;background:var(--d-bg);color:var(--d-ink);font:400 15px/1.45 var(--d-font);-webkit-font-smoothing:antialiased;transition:background-color var(--fd-dur-2) var(--fd-ease-out)}
432.fd-cmd-demo[data-fd-theme="dark"]{--d-bg:#08090B;--d-card:#101014;--d-ink:#F5F5F7;--d-mut:#9A9AA6;--d-line:rgba(255,255,255,.08);--d-accent:#0A84FF;--d-side:#0B0B0F}
433.fd-cmd-demo-side{display:flex;flex-direction:column;gap:2px;padding:20px 12px;background:var(--d-side);border-right:1px solid var(--d-line)}
434.fd-cmd-demo-logo{display:flex;align-items:center;gap:10px;margin:0 8px 20px;font:700 16px/1 var(--d-disp);letter-spacing:-.02em}
435.fd-cmd-demo-logo i{width:20px;height:20px;border-radius:6px;background:#F97316}
436.fd-cmd-demo-side a{display:flex;align-items:center;height:34px;padding:0 10px;border-radius:8px;font:400 14px/1 var(--d-font);color:var(--d-mut)}
437.fd-cmd-demo-side a:hover,.fd-cmd-demo-side a[aria-current]{background:var(--d-line);color:var(--d-ink)}.fd-cmd-demo-side a[aria-current]{font-weight:500}
438.fd-cmd-demo-main{display:flex;flex-direction:column;gap:20px;padding:20px clamp(16px,3vw,40px) 48px;min-width:0}
439.fd-cmd-demo-top{display:flex;gap:12px;align-items:center}
440.fd-cmd-demo-search{display:flex;align-items:center;gap:10px;flex:1;max-width:460px;height:40px;padding:0 8px 0 14px;border-radius:12px;cursor:pointer;text-align:left;background:var(--d-card);color:var(--d-mut);box-shadow:0 0 0 1px var(--d-line),0 1px 2px rgba(0,0,0,.04);font:400 14px/1 var(--d-font);transition:box-shadow var(--fd-dur-2) var(--fd-ease-out)}
441.fd-cmd-demo-search:hover{box-shadow:0 0 0 1px var(--d-mut),0 1px 2px rgba(0,0,0,.04)}.fd-cmd-demo-search span{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
442.fd-cmd-demo-search kbd{display:grid;place-items:center;height:24px;padding:0 8px;border-radius:6px;background:var(--d-line);font:500 11px/1 ui-monospace,"SF Mono",Menlo,monospace}
443.fd-cmd-demo-search:focus-visible,.fd-cmd-demo-theme:focus-visible{outline:2px solid rgba(10,132,255,.7);outline-offset:2px}
444.fd-cmd-demo-theme{cursor:pointer;margin-left:auto;height:32px;padding:0 14px;border-radius:9999px;box-shadow:0 0 0 1px var(--d-line);font:500 13px/1 var(--d-font);color:var(--d-mut)}.fd-cmd-demo-theme:hover{color:var(--d-ink)}
445.fd-cmd-demo-main h1{font:600 24px/1.15 var(--d-disp);letter-spacing:-.02em}
446.fd-cmd-demo-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
447.fd-cmd-demo-kpis div{display:flex;flex-direction:column;gap:8px;padding:16px 18px;border-radius:16px;background:var(--d-card);box-shadow:0 0 0 1px var(--d-line)}
448.fd-cmd-demo-kpis small{font:400 13px/1 var(--d-font);color:var(--d-mut)}.fd-cmd-demo-kpis b{font:600 26px/1 var(--d-disp);letter-spacing:-.03em;font-variant-numeric:tabular-nums}
449.fd-cmd-demo-kpis span{font:500 12px/1 var(--d-font);color:#1C9A44}.fd-cmd-demo-kpis span[data-neg]{color:#E5484D}
450.fd-cmd-demo-table{border-radius:16px;background:var(--d-card);box-shadow:0 0 0 1px var(--d-line);overflow:hidden}
451.fd-cmd-demo-th,.fd-cmd-demo-tr{display:grid;grid-template-columns:110px minmax(0,1.6fr) minmax(0,1fr) 80px;gap:16px;align-items:center;padding:0 20px;height:48px;font:400 14px/1 var(--d-font)}
452.fd-cmd-demo-th{height:40px;font:500 12px/1 var(--d-font);color:var(--d-mut);border-bottom:1px solid var(--d-line)}
453.fd-cmd-demo-tr+.fd-cmd-demo-tr{border-top:1px solid var(--d-line)}.fd-cmd-demo-tr b{font:500 13px/1 ui-monospace,"SF Mono",Menlo,monospace}
454.fd-cmd-demo-tr span:nth-child(3){display:inline-flex;align-items:center;gap:8px}.fd-cmd-demo-tr i{width:6px;height:6px;border-radius:50%;background:var(--d-mut)}
455.fd-cmd-demo-tr [data-k="ok"] i{background:#30D158}.fd-cmd-demo-tr [data-k="warn"] i{background:#FF9F0A}.fd-cmd-demo-tr [data-k="done"] i{background:var(--d-accent)}
456.fd-cmd-demo-log{font:500 13px/1.3 var(--d-font);color:var(--d-accent)}
457@media (max-width:900px){.fd-cmd-demo{grid-template-columns:1fr}.fd-cmd-demo-side{display:none}.fd-cmd-demo-kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.fd-cmd-demo-th,.fd-cmd-demo-tr{grid-template-columns:96px minmax(0,1fr) 72px}.fd-cmd-demo-th span:nth-child(2),.fd-cmd-demo-tr span:nth-child(2){display:none}}
458`;

More Forms. Same file contract.

Input Field

A floating-label input with helper, error and success states, adornments, counter, clear and password reveal.

OTP Input

Six-digit code entry with auto-advance, paste and SMS autofill, error shake and a success cascade.

Payment Form

Card entry with brand detection, live formatting, Luhn validation and a preview that flips to the CVC.