Web InnoventixFreeCode

Error Search Empty

Original · free

no search results page

byWeb InnoventixReact + Tailwind
errorsearchemptypages
Open

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
error-search-empty.tsx
"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">
                        &ldquo;{query}&rdquo;
                      </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&apos;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">
                          &ldquo;{query}&rdquo;
                        </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&apos;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

motion

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 quote

Similar components

Browse all →