Search
Controls & CanvasCommand search with live filtering and an empty state.
Interactive Live Preview
components/ui/search.tsxTypeScript · React 19
1"use client";23import { useState } from "react";4import GlideMenu from "@/components/primitives/GlideMenu";56/* ─────────────────────────────────────────────────────────7 * SEARCH — command search with live filtering.8 * The field, clear action, and results are directly usable.9 * ───────────────────────────────────────────────────────── */1011const 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];2021export default function SearchList() {22 const [query, setQuery] = useState("");23 const results = query24 ? ITEMS.filter((i) => i.toLowerCase().includes(query.toLowerCase()))25 : ITEMS.slice(0, 5);26 const empty = query.length > 2 && results.length === 0;2728 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 <input38 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 <button46 aria-label="Clear search"47 type="button"48 onClick={() => setQuery("")}49 className="flex size-6 items-center justify-center rounded-full text-ink-350 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>5960 {/* 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 <button77 key={item}78 data-menu-row79 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
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | string | "default" | Visual state and layout presentation mode. |
| className | string | "" | Optional Tailwind CSS wrapper class overrides. |