Empty No Results
Original · freeno results with clear filters
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/empty-no-results.json"use client";
import { useId, useMemo, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Framework = "React" | "Vue" | "Svelte" | "Astro";
type Category = "Landing" | "Dashboard" | "Email" | "Docs";
type Template = {
id: string;
name: string;
blurb: string;
category: Category;
framework: Framework;
free: boolean;
installs: string;
};
type Chip = {
id: string;
label: string;
remove: () => void;
};
const CATEGORIES: Category[] = ["Landing", "Dashboard", "Email", "Docs"];
const FRAMEWORKS: Framework[] = ["React", "Vue", "Svelte", "Astro"];
const TEMPLATES: Template[] = [
{
id: "northwind",
name: "Northwind Landing",
blurb: "Long-form product page with a sticky nav, feature grid and a testimonial rail.",
category: "Landing",
framework: "React",
free: true,
installs: "8.2k",
},
{
id: "beacon",
name: "Beacon Pricing",
blurb: "Three-tier pricing table with a monthly/annual switch and a comparison matrix.",
category: "Landing",
framework: "Vue",
free: false,
installs: "3.4k",
},
{
id: "orbit",
name: "Orbit Marketing Site",
blurb: "Five-page marketing shell: home, about, careers, contact and a case-study index.",
category: "Landing",
framework: "Astro",
free: false,
installs: "2.1k",
},
{
id: "meridian",
name: "Meridian Analytics",
blurb: "Revenue dashboard with a date-range picker, cohort table and CSV export.",
category: "Dashboard",
framework: "React",
free: false,
installs: "6.7k",
},
{
id: "slate",
name: "Slate Admin Console",
blurb: "Role-aware admin layout with a command palette and an audit-log drawer.",
category: "Dashboard",
framework: "Svelte",
free: true,
installs: "1.9k",
},
{
id: "pulse",
name: "Pulse Metrics",
blurb: "Realtime uptime board with sparklines, an incident feed and status badges.",
category: "Dashboard",
framework: "Vue",
free: true,
installs: "4.3k",
},
{
id: "postcard",
name: "Postcard Receipt",
blurb: "Transactional receipt email that survives Outlook, Gmail clipping and dark mode.",
category: "Email",
framework: "React",
free: true,
installs: "5.8k",
},
{
id: "digest",
name: "Digest Newsletter",
blurb: "Weekly digest layout with a lead story, three shorts and an unsubscribe footer.",
category: "Email",
framework: "Vue",
free: false,
installs: "2.6k",
},
{
id: "dripline",
name: "Onboarding Drip",
blurb: "Six-step welcome sequence with plain-text fallbacks and merge-tag placeholders.",
category: "Email",
framework: "Svelte",
free: false,
installs: "980",
},
{
id: "handbook",
name: "Handbook Docs",
blurb: "Versioned documentation site with client-side search and a sidebar generator.",
category: "Docs",
framework: "Astro",
free: true,
installs: "7.1k",
},
{
id: "changelog",
name: "Changelog & Roadmap",
blurb: "Dated release notes with tag filters and an RSS feed built at compile time.",
category: "Docs",
framework: "Astro",
free: true,
installs: "3.0k",
},
{
id: "apiref",
name: "API Reference",
blurb: "OpenAPI-driven reference with a try-it panel and copyable request snippets.",
category: "Docs",
framework: "React",
free: false,
installs: "4.9k",
},
];
const SUGGESTIONS = ["dashboard", "pricing", "changelog"];
export default function EmptyNoResults() {
const uid = useId();
const searchRef = useRef<HTMLInputElement>(null);
const reduce = useReducedMotion();
const [query, setQuery] = useState("gantt");
const [category, setCategory] = useState<Category | null>("Email");
const [frameworks, setFrameworks] = useState<Framework[]>(["Svelte"]);
const [freeOnly, setFreeOnly] = useState(true);
const results = useMemo(() => {
const q = query.trim().toLowerCase();
return TEMPLATES.filter((t) => {
if (category && t.category !== category) return false;
if (frameworks.length > 0 && !frameworks.includes(t.framework)) return false;
if (freeOnly && !t.free) return false;
if (q.length > 0) {
const haystack = `${t.name} ${t.blurb} ${t.category}`.toLowerCase();
if (!haystack.includes(q)) return false;
}
return true;
});
}, [query, category, frameworks, freeOnly]);
const toggleFramework = (f: Framework) => {
setFrameworks((prev) => (prev.includes(f) ? prev.filter((x) => x !== f) : [...prev, f]));
};
const clearAll = () => {
setQuery("");
setCategory(null);
setFrameworks([]);
setFreeOnly(false);
searchRef.current?.focus();
};
const applySuggestion = (term: string) => {
setQuery(term);
setCategory(null);
setFrameworks([]);
setFreeOnly(false);
searchRef.current?.focus();
};
const chips: Chip[] = [];
if (query.trim().length > 0) {
chips.push({ id: "q", label: `“${query.trim()}”`, remove: () => setQuery("") });
}
if (category) {
chips.push({ id: "cat", label: category, remove: () => setCategory(null) });
}
for (const f of frameworks) {
chips.push({ id: `fw-${f}`, label: f, remove: () => toggleFramework(f) });
}
if (freeOnly) {
chips.push({ id: "free", label: "Free only", remove: () => setFreeOnly(false) });
}
const hasFilters = chips.length > 0;
const cardMotion = (i: number) =>
reduce
? { initial: { opacity: 1, y: 0 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0 } }
: {
initial: { opacity: 0, y: 12 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.34, delay: Math.min(i * 0.04, 0.28), ease: "easeOut" as const },
};
return (
<section className="relative w-full bg-white px-4 py-20 sm:px-6 sm:py-28 dark:bg-slate-950">
<style>{`
@keyframes enr-sweep {
0% { transform: translateX(-140%) skewX(-18deg); opacity: 0; }
35% { opacity: 0.7; }
100% { transform: translateX(240%) skewX(-18deg); opacity: 0; }
}
@keyframes enr-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-5px); }
}
@keyframes enr-ring {
0% { transform: scale(0.85); opacity: 0.45; }
100% { transform: scale(1.4); opacity: 0; }
}
.enr-sweep-el { animation: enr-sweep 3.6s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
.enr-float-el { animation: enr-float 5s ease-in-out infinite; }
.enr-ring-el { animation: enr-ring 2.8s ease-out infinite; }
@media (prefers-reduced-motion: reduce) {
.enr-sweep-el, .enr-float-el, .enr-ring-el { animation: none !important; opacity: 0.25; }
.enr-sweep-el { opacity: 0; }
}
`}</style>
<div className="mx-auto max-w-5xl">
<header className="mb-8">
<span className="inline-flex items-center rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
Template library
</span>
<h2 className="mt-4 text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-white">
Find a starting point
</h2>
<p className="mt-3 max-w-2xl text-base leading-relaxed text-slate-600 dark:text-slate-400">
Twelve production templates across four frameworks. Narrow the list, and when nothing
matches you can drop filters one at a time instead of starting over.
</p>
</header>
<div className="overflow-hidden rounded-3xl border border-slate-200 bg-slate-50/70 shadow-sm dark:border-slate-800 dark:bg-slate-900/40">
{/* Toolbar */}
<div className="border-b border-slate-200 p-4 sm:p-6 dark:border-slate-800">
<label htmlFor={`${uid}-search`} className="sr-only">
Search templates
</label>
<div className="relative">
<span className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-4 text-slate-400 dark:text-slate-500">
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="none" aria-hidden="true">
<circle cx="9" cy="9" r="6" stroke="currentColor" strokeWidth="1.8" />
<path
d="M13.5 13.5 17 17"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
</span>
<input
id={`${uid}-search`}
ref={searchRef}
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Escape" && query.length > 0) {
e.preventDefault();
setQuery("");
}
}}
placeholder="Search by name, purpose or category…"
autoComplete="off"
className="w-full rounded-xl border border-slate-300 bg-white py-3 pl-11 pr-11 text-sm text-slate-900 placeholder:text-slate-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100 dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900"
/>
{query.length > 0 && (
<button
type="button"
onClick={() => {
setQuery("");
searchRef.current?.focus();
}}
aria-label="Clear the search field"
className="absolute inset-y-0 right-0 mr-2 my-2 flex w-8 items-center justify-center rounded-lg text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 focus: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 20 20" className="h-4 w-4" fill="none" aria-hidden="true">
<path
d="M6 6l8 8M14 6l-8 8"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
</button>
)}
</div>
{/* Filter controls */}
<div className="mt-4 flex flex-wrap items-center gap-x-6 gap-y-4">
<fieldset>
<legend className="sr-only">Category</legend>
<div className="flex flex-wrap items-center gap-1 rounded-xl border border-slate-300 bg-white p-1 dark:border-slate-700 dark:bg-slate-950">
{CATEGORIES.map((c) => (
<label key={c} className="cursor-pointer">
<input
type="radio"
name={`${uid}-category`}
value={c}
checked={category === c}
onChange={() => setCategory(c)}
className="peer sr-only"
/>
<span className="block rounded-lg px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors peer-checked:bg-slate-900 peer-checked:text-white peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-1 peer-focus-visible:ring-offset-white hover:text-slate-900 dark:text-slate-400 dark:peer-checked:bg-slate-100 dark:peer-checked:text-slate-900 dark:peer-focus-visible:ring-offset-slate-950 dark:hover:text-slate-100">
{c}
</span>
</label>
))}
</div>
</fieldset>
<fieldset>
<legend className="sr-only">Framework</legend>
<div className="flex flex-wrap items-center gap-2">
{FRAMEWORKS.map((f) => (
<label key={f} className="cursor-pointer">
<input
type="checkbox"
checked={frameworks.includes(f)}
onChange={() => toggleFramework(f)}
className="peer sr-only"
/>
<span className="block rounded-full border border-slate-300 px-3 py-1.5 text-xs font-medium text-slate-600 transition-colors peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-slate-50 hover:border-slate-400 dark:border-slate-700 dark:text-slate-400 dark:peer-checked:border-indigo-400/60 dark:peer-checked:bg-indigo-500/15 dark:peer-checked:text-indigo-300 dark:peer-focus-visible:ring-offset-slate-900 dark:hover:border-slate-600">
{f}
</span>
</label>
))}
</div>
</fieldset>
<label className="flex cursor-pointer items-center gap-2">
<input
type="checkbox"
checked={freeOnly}
onChange={(e) => setFreeOnly(e.target.checked)}
className="peer sr-only"
/>
<span className="relative h-5 w-9 rounded-full bg-slate-300 transition-colors peer-checked:bg-emerald-500 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-slate-50 dark:bg-slate-700 dark:peer-checked:bg-emerald-500 dark:peer-focus-visible:ring-offset-slate-900">
<span
className="absolute left-0.5 top-0.5 h-4 w-4 rounded-full bg-white shadow transition-transform"
style={{ transform: freeOnly ? "translateX(16px)" : "translateX(0)" }}
/>
</span>
<span className="text-xs font-medium text-slate-600 dark:text-slate-400">
Free only
</span>
</label>
</div>
</div>
{/* Active filters + count */}
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 bg-white/60 px-4 py-3 sm:px-6 dark:border-slate-800 dark:bg-slate-950/30">
<div className="flex flex-wrap items-center gap-2">
<span className="text-xs font-medium text-slate-500 dark:text-slate-500">
{hasFilters ? "Filters:" : "No filters applied"}
</span>
{chips.map((chip) => (
<button
key={chip.id}
type="button"
onClick={chip.remove}
aria-label={`Remove filter ${chip.label}`}
className="group inline-flex items-center gap-1.5 rounded-full border border-slate-300 bg-white py-1 pl-2.5 pr-1.5 text-xs font-medium text-slate-700 transition-colors hover:border-rose-300 hover:bg-rose-50 hover:text-rose-700 focus: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-rose-500/40 dark:hover:bg-rose-500/10 dark:hover:text-rose-300 dark:focus-visible:ring-offset-slate-950"
>
{chip.label}
<span
aria-hidden="true"
className="flex h-4 w-4 items-center justify-center rounded-full bg-slate-100 text-slate-500 transition-colors group-hover:bg-rose-200 group-hover:text-rose-700 dark:bg-slate-800 dark:text-slate-400 dark:group-hover:bg-rose-500/25 dark:group-hover:text-rose-200"
>
<svg viewBox="0 0 12 12" className="h-2.5 w-2.5" fill="none">
<path
d="M3 3l6 6M9 3l-6 6"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
/>
</svg>
</span>
</button>
))}
{hasFilters && (
<button
type="button"
onClick={clearAll}
className="rounded-full px-2 py-1 text-xs font-semibold text-indigo-600 underline-offset-2 transition-colors hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:text-indigo-400 dark:focus-visible:ring-offset-slate-950"
>
Clear all
</button>
)}
</div>
<p
role="status"
aria-live="polite"
aria-atomic="true"
className="text-xs font-medium tabular-nums text-slate-500 dark:text-slate-500"
>
{results.length} of {TEMPLATES.length} templates
</p>
</div>
{/* Results / empty state */}
<div className="p-4 sm:p-6">
<AnimatePresence mode="wait" initial={false}>
{results.length === 0 ? (
<motion.div
key="empty"
initial={reduce ? { opacity: 1 } : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 1 } : { opacity: 0, y: -8 }}
transition={{ duration: reduce ? 0 : 0.28, ease: "easeOut" }}
className="flex flex-col items-center px-2 py-10 text-center sm:py-14"
>
<div className="relative mb-6 flex h-32 w-32 items-center justify-center overflow-hidden rounded-full bg-slate-100/80 dark:bg-slate-800/50">
<span
aria-hidden="true"
className="enr-ring-el absolute inset-4 rounded-full border border-indigo-400/40 dark:border-indigo-400/30"
/>
<span
aria-hidden="true"
className="enr-sweep-el absolute inset-y-0 -left-1/3 w-1/3 bg-gradient-to-r from-transparent via-white/70 to-transparent dark:via-white/12"
/>
<svg
viewBox="0 0 120 120"
aria-hidden="true"
className="enr-float-el relative h-24 w-24"
fill="none"
>
<rect
x="24"
y="30"
width="54"
height="13"
rx="4"
fill="currentColor"
className="text-slate-300 dark:text-slate-700"
/>
<rect
x="24"
y="51"
width="42"
height="13"
rx="4"
fill="currentColor"
className="text-slate-300 dark:text-slate-700"
/>
<rect
x="24"
y="72"
width="34"
height="13"
rx="4"
fill="currentColor"
className="text-slate-300 dark:text-slate-700"
/>
<circle
cx="76"
cy="60"
r="22"
fill="currentColor"
className="text-white dark:text-slate-900"
/>
<circle
cx="76"
cy="60"
r="22"
stroke="currentColor"
strokeWidth="3.5"
className="text-indigo-500 dark:text-indigo-400"
/>
<path
d="M92 76 L104 88"
stroke="currentColor"
strokeWidth="4.5"
strokeLinecap="round"
className="text-indigo-500 dark:text-indigo-400"
/>
<path
d="M69 53 L83 67 M83 53 L69 67"
stroke="currentColor"
strokeWidth="3.5"
strokeLinecap="round"
className="text-rose-500 dark:text-rose-400"
/>
</svg>
</div>
<h3 className="text-xl font-semibold tracking-tight text-slate-900 sm:text-2xl dark:text-white">
Nothing matches those filters
</h3>
<p className="mt-2 max-w-md text-sm leading-relaxed text-slate-600 dark:text-slate-400">
{hasFilters ? (
<>
No template fits{" "}
<span className="font-medium text-slate-900 dark:text-slate-200">
{chips.map((c) => c.label).join(" + ")}
</span>
. Drop a filter above, or clear them all and start from the full set of 12.
</>
) : (
"The library is empty right now. Check back after the next release drop."
)}
</p>
{hasFilters && (
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
<button
type="button"
onClick={clearAll}
className="inline-flex items-center gap-2 rounded-xl bg-slate-900 px-4 py-2.5 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-900"
>
<svg viewBox="0 0 20 20" className="h-4 w-4" fill="none" aria-hidden="true">
<path
d="M15.5 5.5a6.5 6.5 0 1 0 1.4 4.5"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
<path
d="M16.5 2.8v3.2h-3.2"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
Clear all filters
</button>
{chips.length > 1 && (
<button
type="button"
onClick={chips[chips.length - 1].remove}
className="rounded-xl border border-slate-300 px-4 py-2.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-white focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-900"
>
Remove “{chips[chips.length - 1].label}” only
</button>
)}
</div>
)}
<div className="mt-8 border-t border-slate-200 pt-6 dark:border-slate-800">
<p className="text-xs font-medium uppercase tracking-wide text-slate-500 dark:text-slate-500">
Try one of these instead
</p>
<div className="mt-3 flex flex-wrap items-center justify-center gap-2">
{SUGGESTIONS.map((s) => (
<button
key={s}
type="button"
onClick={() => applySuggestion(s)}
className="rounded-full border border-slate-300 bg-white px-3 py-1.5 text-xs font-medium text-slate-700 transition-colors hover:border-indigo-400 hover:text-indigo-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:border-indigo-400/60 dark:hover:text-indigo-300 dark:focus-visible:ring-offset-slate-900"
>
{s}
</button>
))}
</div>
</div>
</motion.div>
) : (
<motion.ul
key="results"
initial={reduce ? { opacity: 1 } : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={reduce ? { opacity: 1 } : { opacity: 0 }}
transition={{ duration: reduce ? 0 : 0.2 }}
className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"
>
{results.map((t, i) => (
<motion.li
key={t.id}
initial={cardMotion(i).initial}
animate={cardMotion(i).animate}
transition={cardMotion(i).transition}
className="flex flex-col rounded-2xl border border-slate-200 bg-white p-4 transition-colors hover:border-slate-300 dark:border-slate-800 dark:bg-slate-950/60 dark:hover:border-slate-700"
>
<div className="flex items-start justify-between gap-2">
<h3 className="text-sm font-semibold text-slate-900 dark:text-white">
{t.name}
</h3>
<span
className={
t.free
? "shrink-0 rounded-full bg-emerald-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-300"
: "shrink-0 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700 dark:bg-amber-500/10 dark:text-amber-300"
}
>
{t.free ? "Free" : "Pro"}
</span>
</div>
<p className="mt-2 flex-1 text-xs leading-relaxed text-slate-600 dark:text-slate-400">
{t.blurb}
</p>
<div className="mt-4 flex items-center gap-2 text-[11px] font-medium text-slate-500 dark:text-slate-500">
<span className="rounded-md bg-slate-100 px-1.5 py-0.5 dark:bg-slate-800">
{t.framework}
</span>
<span className="rounded-md bg-slate-100 px-1.5 py-0.5 dark:bg-slate-800">
{t.category}
</span>
<span className="ml-auto tabular-nums">{t.installs} installs</span>
</div>
</motion.li>
))}
</motion.ul>
)}
</AnimatePresence>
</div>
</div>
</div>
</section>
);
}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 →
Empty No Data
Originalempty data state with illustration

Empty Cart
Originalempty shopping cart state

Empty Inbox
Originalempty inbox / all caught up

Empty First Run
Originalfirst-run get started empty state

Empty Notifications
Originalno notifications state

Empty Error
Originalsomething went wrong empty state

Empty No Connection
Originalno connection empty state

Empty Folder
Originalempty folder with upload CTA

Empty Success
Originalsuccess / done empty state

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.

