Error Search Empty
Original · freeno search results page
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/error-search-empty.json"use client";
import { useId, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Category =
| "Account"
| "Billing"
| "Security"
| "Teams"
| "Integrations"
| "Data"
| "Projects"
| "Developers";
type Article = {
id: string;
title: string;
category: Category;
blurb: string;
minutes: number;
keywords: string[];
};
const CATEGORIES: Category[] = [
"Account",
"Billing",
"Security",
"Teams",
"Integrations",
"Data",
"Projects",
"Developers",
];
const ARTICLES: Article[] = [
{
id: "reset-password",
title: "Reset your password",
category: "Account",
blurb:
"Send a secure reset link to your email and set a new password in under a minute.",
minutes: 2,
keywords: ["reset", "password", "forgot", "login", "sign in", "recover"],
},
{
id: "invite-teammates",
title: "Invite teammates to a workspace",
category: "Teams",
blurb:
"Add members by email, set their role, and manage seats without leaving the dashboard.",
minutes: 3,
keywords: ["invite", "teammates", "team", "members", "seats", "workspace"],
},
{
id: "two-factor",
title: "Set up two-factor authentication",
category: "Security",
blurb:
"Protect your account with an authenticator app or SMS codes at every sign-in.",
minutes: 4,
keywords: ["two-factor", "2fa", "authentication", "security", "otp", "code"],
},
{
id: "export-data",
title: "Export your data as CSV",
category: "Data",
blurb:
"Download a portable copy of projects, tasks, and comments for backups or migration.",
minutes: 3,
keywords: ["export", "data", "csv", "download", "backup", "migrate"],
},
{
id: "slack-notifications",
title: "Connect Slack notifications",
category: "Integrations",
blurb:
"Pipe mentions, due dates, and status changes straight into any Slack channel.",
minutes: 5,
keywords: ["slack", "notifications", "integration", "connect", "alerts"],
},
{
id: "change-billing-plan",
title: "Change your billing plan",
category: "Billing",
blurb:
"Upgrade, downgrade, or switch to annual billing with prorated charges applied instantly.",
minutes: 3,
keywords: ["billing", "plan", "upgrade", "downgrade", "invoice", "payment"],
},
{
id: "cancel-subscription",
title: "Cancel your subscription",
category: "Billing",
blurb:
"End your plan, keep read-only access until the period closes, and export first.",
minutes: 2,
keywords: ["cancel", "subscription", "refund", "downgrade", "close account"],
},
{
id: "project-templates",
title: "Create a project template",
category: "Projects",
blurb:
"Turn any project into a reusable template so new work starts fully structured.",
minutes: 4,
keywords: ["project", "template", "reusable", "workflow", "duplicate"],
},
{
id: "keyboard-shortcuts",
title: "Keyboard shortcuts reference",
category: "Projects",
blurb:
"Move faster with the full list of shortcuts for navigation, editing, and search.",
minutes: 2,
keywords: ["keyboard", "shortcuts", "hotkeys", "productivity", "commands"],
},
{
id: "delete-account",
title: "Delete your account permanently",
category: "Account",
blurb:
"Understand what gets erased, the 30-day grace window, and how to confirm deletion.",
minutes: 3,
keywords: ["delete", "account", "remove", "erase", "close", "gdpr"],
},
{
id: "api-keys",
title: "Manage your API keys",
category: "Developers",
blurb:
"Generate scoped tokens, rotate secrets, and revoke access for any integration.",
minutes: 4,
keywords: ["api", "keys", "token", "developer", "webhook", "secret"],
},
{
id: "restore-deleted",
title: "Restore deleted items",
category: "Data",
blurb:
"Recover tasks and projects from the trash for up to 30 days after removal.",
minutes: 2,
keywords: ["restore", "deleted", "trash", "recover", "undo", "data"],
},
];
const POPULAR: string[] = [
"reset password",
"cancel subscription",
"export data",
"invite teammates",
"two-factor authentication",
"api keys",
];
const KNOWN_PHRASES: string[] = [
"reset password",
"cancel subscription",
"change billing plan",
"export data",
"invite teammates",
"two-factor authentication",
"connect slack notifications",
"keyboard shortcuts",
"delete account",
"api keys",
"restore deleted items",
"project template",
];
const CATEGORY_BADGE: Record<Category, string> = {
Account:
"bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
Billing:
"bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
Security:
"bg-rose-100 text-rose-700 dark:bg-rose-500/15 dark:text-rose-300",
Teams:
"bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300",
Integrations:
"bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
Data:
"bg-indigo-100 text-indigo-700 dark:bg-indigo-500/15 dark:text-indigo-300",
Projects:
"bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300",
Developers:
"bg-slate-200 text-slate-700 dark:bg-slate-500/20 dark:text-slate-300",
};
function editDistance(a: string, b: string): number {
const m = a.length;
const n = b.length;
const dp: number[] = Array.from({ length: n + 1 }, (_, i) => i);
for (let i = 1; i <= m; i++) {
let prev = dp[0];
dp[0] = i;
for (let j = 1; j <= n; j++) {
const tmp = dp[j];
dp[j] = Math.min(
dp[j] + 1,
dp[j - 1] + 1,
prev + (a[i - 1] === b[j - 1] ? 0 : 1),
);
prev = tmp;
}
}
return dp[n];
}
function matchesQuery(article: Article, query: string): boolean {
const hay = (
article.title +
" " +
article.blurb +
" " +
article.keywords.join(" ")
).toLowerCase();
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
return tokens.every((t) => hay.includes(t));
}
function SearchIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<circle cx="11" cy="11" r="7" />
<line x1="16.5" y1="16.5" x2="21" y2="21" />
</svg>
);
}
export default function ErrorSearchEmpty() {
const reduce = useReducedMotion();
const inputId = useId();
const statusId = useId();
const inputRef = useRef<HTMLInputElement>(null);
const [input, setInput] = useState<string>("cancle subscriton");
const [category, setCategory] = useState<Category | "All">("All");
const query = input.trim();
const hasQuery = query.length > 0;
const hasFilter = hasQuery || category !== "All";
const results = useMemo<Article[]>(() => {
return ARTICLES.filter((a) => {
const inCategory = category === "All" || a.category === category;
const inQuery = !hasQuery || matchesQuery(a, query);
return inCategory && inQuery;
});
}, [query, hasQuery, category]);
const suggestion = useMemo<string | null>(() => {
if (!hasQuery || results.length > 0) return null;
const normalized = query.toLowerCase();
let best: string | null = null;
let bestDist = Infinity;
for (const phrase of KNOWN_PHRASES) {
const dist = editDistance(normalized, phrase);
if (dist < bestDist) {
bestDist = dist;
best = phrase;
}
}
if (best === null) return null;
const threshold = Math.max(2, Math.ceil(normalized.length * 0.45));
return bestDist > 0 && bestDist <= threshold ? best : null;
}, [hasQuery, query, results.length]);
const state: "browse" | "empty" | "results" = !hasFilter
? "browse"
: results.length === 0
? "empty"
: "results";
const statusText =
state === "browse"
? "Enter a search term to find help articles."
: state === "empty"
? `No results found for ${hasQuery ? `“${query}”` : "this filter"}.`
: `${results.length} ${results.length === 1 ? "result" : "results"} found${
hasQuery ? ` for “${query}”` : ""
}.`;
function focusInput() {
window.requestAnimationFrame(() => inputRef.current?.focus());
}
function runSearch(term: string) {
setInput(term);
setCategory("All");
focusInput();
}
function clearSearch() {
setInput("");
focusInput();
}
const panelMotion = reduce
? {}
: {
initial: { opacity: 0, y: 10 },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: -8 },
transition: { duration: 0.28, ease: [0.22, 1, 0.36, 1] as const },
};
return (
<section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white px-4 py-16 text-slate-900 sm:px-6 sm:py-24 dark:from-slate-950 dark:to-slate-900 dark:text-slate-100">
<style>{`
@keyframes ese-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
@keyframes ese-blob { 0%, 100% { transform: scale(1); opacity: .55; } 50% { transform: scale(1.08); opacity: .8; } }
.ese-float { animation: ese-float 5.5s ease-in-out infinite; }
.ese-blob { animation: ese-blob 8s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.ese-float, .ese-blob { animation: none !important; }
}
`}</style>
<div className="relative mx-auto max-w-3xl">
{/* Header */}
<div className="text-center">
<p className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-medium uppercase tracking-wider text-slate-500 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">
<span
aria-hidden="true"
className="h-1.5 w-1.5 rounded-full bg-indigo-500"
/>
Halcyon Help Center
</p>
<h1 className="mt-5 text-3xl font-bold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
How can we help you today?
</h1>
<p className="mx-auto mt-3 max-w-xl text-base text-slate-600 dark:text-slate-400">
Search our guides and documentation, or browse popular topics below.
</p>
</div>
{/* Search form */}
<form
role="search"
className="mt-8"
onSubmit={(e) => {
e.preventDefault();
focusInput();
}}
>
<label htmlFor={inputId} className="sr-only">
Search help articles
</label>
<div className="flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<SearchIcon className="pointer-events-none absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-slate-400 dark:text-slate-500" />
<input
id={inputId}
ref={inputRef}
type="search"
value={input}
autoComplete="off"
spellCheck={false}
aria-describedby={statusId}
placeholder="Search for articles, e.g. reset password"
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape" && input.length > 0) {
e.preventDefault();
clearSearch();
}
}}
className="w-full rounded-xl border border-slate-300 bg-white py-3 pl-12 pr-11 text-base text-slate-900 shadow-sm outline-none transition placeholder:text-slate-400 focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-900 dark:text-white dark:placeholder:text-slate-500"
/>
{input.length > 0 ? (
<button
type="button"
onClick={clearSearch}
aria-label="Clear search"
className="absolute right-2.5 top-1/2 flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-lg text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-500 dark:hover:bg-slate-800 dark:hover:text-slate-200"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
aria-hidden="true"
className="h-4 w-4"
>
<line x1="6" y1="6" x2="18" y2="18" />
<line x1="18" y1="6" x2="6" y2="18" />
</svg>
</button>
) : null}
</div>
<button
type="submit"
className="inline-flex items-center justify-center gap-2 rounded-xl bg-indigo-600 px-6 py-3 text-base font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white active:scale-[0.98] dark:focus-visible:ring-offset-slate-950"
>
<SearchIcon className="h-5 w-5" />
Search
</button>
</div>
</form>
{/* Category filters */}
<div className="mt-5">
<div
role="group"
aria-label="Filter by category"
className="flex flex-wrap items-center justify-center gap-2"
>
<FilterPill
label="All topics"
active={category === "All"}
onClick={() => setCategory("All")}
/>
{CATEGORIES.map((c) => (
<FilterPill
key={c}
label={c}
active={category === c}
onClick={() => setCategory(category === c ? "All" : c)}
/>
))}
</div>
</div>
{/* Live status for assistive tech */}
<p id={statusId} role="status" aria-live="polite" className="sr-only">
{statusText}
</p>
{/* Results region */}
<div
aria-label="Search results"
className="mt-10"
>
<AnimatePresence mode="wait" initial={false}>
{state === "empty" ? (
<motion.div key="empty" {...panelMotion} className="text-center">
<div className="relative mx-auto flex h-44 w-full max-w-xs items-center justify-center">
<span
aria-hidden="true"
className="ese-blob absolute h-40 w-40 rounded-full bg-indigo-500/10 blur-2xl dark:bg-indigo-400/10"
/>
<svg
viewBox="0 0 220 180"
role="img"
aria-label="Empty search results"
className="ese-float relative h-40 w-auto"
>
<g
className="text-slate-300 dark:text-slate-600"
stroke="currentColor"
fill="none"
strokeWidth={4}
strokeLinecap="round"
>
<rect
x="26"
y="30"
width="150"
height="112"
rx="16"
strokeDasharray="9 13"
/>
<line x1="50" y1="66" x2="126" y2="66" />
<line x1="50" y1="92" x2="108" y2="92" />
<line x1="50" y1="118" x2="92" y2="118" />
</g>
<g
className="text-indigo-500 dark:text-indigo-400"
stroke="currentColor"
strokeWidth={6}
strokeLinecap="round"
fill="none"
>
<circle
cx="150"
cy="118"
r="34"
className="fill-white dark:fill-slate-950"
/>
<line x1="174" y1="142" x2="200" y2="168" />
<line x1="136" y1="118" x2="164" y2="118" />
</g>
</svg>
</div>
<h2 className="mt-4 text-xl font-semibold text-slate-900 dark:text-white">
No results{" "}
{hasQuery ? (
<>
for{" "}
<span className="text-indigo-600 dark:text-indigo-400">
“{query}”
</span>
</>
) : (
<>in this category</>
)}
</h2>
<p className="mx-auto mt-2 max-w-md text-sm text-slate-600 dark:text-slate-400">
We couldn't find any articles matching your search. Check
the spelling, try a broader term, or browse the popular topics
below.
</p>
{suggestion ? (
<p className="mt-4 text-sm text-slate-600 dark:text-slate-400">
Did you mean{" "}
<button
type="button"
onClick={() => runSearch(suggestion)}
className="rounded font-semibold text-indigo-600 underline decoration-dotted underline-offset-4 transition hover:text-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-indigo-400"
>
{suggestion}
</button>
?
</p>
) : null}
<PopularSearches onSelect={runSearch} className="mt-8" />
</motion.div>
) : state === "results" ? (
<motion.div key="results" {...panelMotion}>
<div className="flex items-baseline justify-between gap-3">
<h2 className="text-sm font-semibold text-slate-900 dark:text-white">
{results.length}{" "}
{results.length === 1 ? "result" : "results"}
{hasQuery ? (
<>
{" "}
for{" "}
<span className="text-indigo-600 dark:text-indigo-400">
“{query}”
</span>
</>
) : (
<>
{" "}
in{" "}
<span className="text-indigo-600 dark:text-indigo-400">
{category}
</span>
</>
)}
</h2>
{hasFilter ? (
<button
type="button"
onClick={() => {
setInput("");
setCategory("All");
focusInput();
}}
className="rounded text-xs font-medium text-slate-500 underline-offset-4 transition hover:text-indigo-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:text-indigo-400"
>
Reset
</button>
) : null}
</div>
<ul className="mt-4 space-y-3">
{results.map((a, i) => (
<motion.li
key={a.id}
initial={reduce ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={
reduce
? undefined
: { duration: 0.22, delay: i * 0.035 }
}
>
<a
href={`#${a.id}`}
className="group flex items-start gap-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm transition hover:border-indigo-300 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-800 dark:bg-slate-900 dark:hover:border-indigo-500/60 dark:focus-visible:ring-offset-slate-950"
>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<span
className={`rounded-md px-2 py-0.5 text-xs font-medium ${CATEGORY_BADGE[a.category]}`}
>
{a.category}
</span>
<span className="text-xs text-slate-400 dark:text-slate-500">
{a.minutes} min read
</span>
</div>
<h3 className="mt-1.5 truncate text-base font-semibold text-slate-900 dark:text-white">
{a.title}
</h3>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">
{a.blurb}
</p>
</div>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="mt-1 h-5 w-5 shrink-0 text-slate-300 transition group-hover:translate-x-0.5 group-hover:text-indigo-500 dark:text-slate-600"
>
<path d="M9 6l6 6-6 6" />
</svg>
</a>
</motion.li>
))}
</ul>
</motion.div>
) : (
<motion.div key="browse" {...panelMotion} className="text-center">
<p className="text-sm text-slate-600 dark:text-slate-400">
Start typing to search across{" "}
<span className="font-semibold text-slate-900 dark:text-white">
{ARTICLES.length}
</span>{" "}
articles, or pick a popular topic.
</p>
<PopularSearches onSelect={runSearch} className="mt-6" />
</motion.div>
)}
</AnimatePresence>
</div>
{/* Support footer */}
<div className="mt-14 rounded-2xl border border-slate-200 bg-white/60 p-6 text-center dark:border-slate-800 dark:bg-slate-900/60">
<p className="text-sm font-medium text-slate-900 dark:text-white">
Still can't find what you need?
</p>
<p className="mt-1 text-sm text-slate-600 dark:text-slate-400">
Our support team replies within one business day.
</p>
<a
href="mailto:support@halcyon.app"
className="mt-4 inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-5 py-2.5 text-sm font-semibold text-slate-800 shadow-sm transition hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:hover:border-indigo-500 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className="h-4 w-4"
>
<rect x="3" y="5" width="18" height="14" rx="2" />
<path d="M3 7l9 6 9-6" />
</svg>
Contact support
</a>
</div>
</div>
</section>
);
}
function FilterPill({
label,
active,
onClick,
}: {
label: string;
active: boolean;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
aria-pressed={active}
className={
active
? "rounded-full border border-indigo-600 bg-indigo-600 px-3.5 py-1.5 text-sm font-medium text-white shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950"
: "rounded-full border border-slate-300 bg-white px-3.5 py-1.5 text-sm font-medium text-slate-700 transition hover:border-indigo-400 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-indigo-500 dark:hover:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
}
>
{label}
</button>
);
}
function PopularSearches({
onSelect,
className,
}: {
onSelect: (term: string) => void;
className?: string;
}) {
return (
<div className={className}>
<p className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400">
Popular searches
</p>
<div className="mt-3 flex flex-wrap justify-center gap-2">
{POPULAR.map((term) => (
<button
key={term}
type="button"
onClick={() => onSelect(term)}
className="inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-slate-50 px-3.5 py-1.5 text-sm text-slate-700 transition hover:border-indigo-300 hover:bg-indigo-50 hover:text-indigo-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-200 dark:hover:border-indigo-500/60 dark:hover:bg-indigo-500/10 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-950"
>
<SearchIcon className="h-3.5 w-3.5 text-slate-400 dark:text-slate-500" />
{term}
</button>
))}
</div>
</div>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Error 404
Originalfriendly 404 page with illustration and CTA

Error 500
Original500 server error page

Error 403
Original403 access denied page

Error Offline
Originaloffline / no-connection state

Error Maintenance
Originalscheduled maintenance page

Error Coming Soon
Originalcoming soon page with countdown and email

Error Under Construction
Originalunder construction page

Error Expired
Originallink/session expired page

Error Generic
Originalgeneric error page with retry

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

