Search

Controls & Canvas

Command search with live filtering and an empty state.

Interactive Live Preview
components/ui/search.tsxTypeScript · React 19
1"use client";
2
3import { useState } from "react";
4import GlideMenu from "@/components/primitives/GlideMenu";
5
6/* ─────────────────────────────────────────────────────────
7 * SEARCH — command search with live filtering.
8 * The field, clear action, and results are directly usable.
9 * ───────────────────────────────────────────────────────── */
10
11const ITEMS = [
12 "Forecast summer demand",
13 "Find waffle cone suppliers",
14 "Compare seasonal flavors",
15 "Draft flavor launch plan",
16 "Check cold-chain status",
17 "Audit sugar costs",
18 "Retire low sellers",
19];
20
21export default function SearchList() {
22 const [query, setQuery] = useState("");
23 const results = query
24 ? ITEMS.filter((i) => i.toLowerCase().includes(query.toLowerCase()))
25 : ITEMS.slice(0, 5);
26 const empty = query.length > 2 && results.length === 0;
27
28 return (
29 <div className="flex min-h-[248px] w-full max-w-72 flex-col items-stretch">
30 <div className="w-full self-start overflow-hidden rounded-card bg-surface shadow-raised">
31 {/* input row */}
32 <div className="flex h-10 items-center gap-2 border-b border-line px-3 transition-colors duration-100 hover:bg-hover">
33 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--ink-3)" strokeWidth="2" strokeLinecap="round" className="shrink-0">
34 <circle cx="11" cy="11" r="7" />
35 <path d="M21 21l-4.3-4.3" />
36 </svg>
37 <input
38 value={query}
39 onChange={(event) => setQuery(event.target.value)}
40 placeholder="Search flavors…"
41 aria-label="Search flavors"
42 className="min-w-0 flex-1 bg-transparent text-[13px] text-ink outline-none placeholder:text-ink-3"
43 />
44 {query && (
45 <button
46 aria-label="Clear search"
47 type="button"
48 onClick={() => setQuery("")}
49 className="flex size-6 items-center justify-center rounded-full text-ink-3
50 transition-colors duration-100 hover:bg-line/70 hover:text-ink"
51 style={{ animation: "fade-in 150ms ease-out both" }}
52 >
53 <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
54 <path d="M18 6L6 18M6 6l12 12" />
55 </svg>
56 </button>
57 )}
58 </div>
59
60 {/* results / empty state */}
61 {empty ? (
62 <div className="flex flex-col items-center justify-center gap-1 px-4 py-8" style={{ animation: "fade-in 250ms ease-out both" }}>
63 <span className="mb-1.5 flex size-8 items-center justify-center rounded-control bg-inset text-ink-3 shadow-hairline">
64 <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round">
65 <circle cx="11" cy="11" r="7" />
66 <path d="M21 21l-4.3-4.3" />
67 </svg>
68 </span>
69 <span className="text-[13px] font-medium text-ink">No results found</span>
70 <span className="text-[12px] text-ink-3">Adjust your search to try again</span>
71 </div>
72 ) : (
73 <div className="p-1">
74 <GlideMenu className="flex flex-col gap-px" highlightClassName="inset-x-0 rounded-[6px] bg-hover">
75 {results.map((item) => (
76 <button
77 key={item}
78 data-menu-row
79 type="button"
80 onClick={() => setQuery(item)}
81 className="relative z-10 flex h-8 w-full items-center rounded-[6px] px-2 text-left text-[13px] text-ink"
82 style={{ animation: "fade-in 200ms ease-out both" }}
83 >
84 {item}
85 </button>
86 ))}
87 </GlideMenu>
88 </div>
89 )}
90 </div>
91 </div>
92 );
93}
94

Props & Specification

PropTypeDefaultDescription
variantstring&quot;default&quot;Visual state and layout presentation mode.
classNamestring""Optional Tailwind CSS wrapper class overrides.

Related Components in Controls & Canvas