Docs
FAQ Search

FAQ Search

A searchable FAQ section with real-time filtering. Features instant search, category badges, result counts, and empty state handling.

Frequently Asked Questions

Search through our knowledge base to find answers

Can't find what you're looking for?

Our support team is ready to help you with any questions

Contact Support

Installation

Copy and paste the following code into your project.

"use client";
 
import { useState, useMemo } from "react";
import {
  Accordion,
  AccordionContent,
  AccordionItem,
  AccordionTrigger,
} from "@/components/ui/accordion";
import { Search, X } from "lucide-react";
 
interface FAQItem {
  id: string;
  question: string;
  answer: string;
  category?: string;
}
 
interface FAQSearchProps {
  title?: string;
  description?: string;
  faqs: FAQItem[];
  placeholder?: string;
  className?: string;
}
 
export function FAQSearch({
  title = "Frequently Asked Questions",
  description = "Search through our knowledge base to find answers",
  faqs,
  placeholder = "Search for questions...",
  className = "",
}: FAQSearchProps) {
  const [searchQuery, setSearchQuery] = useState("");
 
  // Filter FAQs based on search query
  const filteredFAQs = useMemo(() => {
    if (!searchQuery.trim()) {
      return faqs;
    }
 
    const query = searchQuery.toLowerCase();
    return faqs.filter(
      (faq) =>
        faq.question.toLowerCase().includes(query) ||
        faq.answer.toLowerCase().includes(query) ||
        faq.category?.toLowerCase().includes(query),
    );
  }, [faqs, searchQuery]);
 
  const handleClear = () => {
    setSearchQuery("");
  };
 
  return (
    <section className={`w-full bg-black py-20 ${className}`}>
      <div className="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
        {/* Header */}
        <div className="mb-12 text-center">
          <h2 className="mb-4 text-4xl font-bold text-white sm:text-5xl">
            {title}
          </h2>
          <p className="text-lg text-zinc-400">{description}</p>
        </div>
 
        {/* Search Bar */}
        <div className="mb-8">
          <div className="relative">
            <Search className="absolute left-4 top-1/2 h-5 w-5 -translate-y-1/2 text-zinc-500" />
            <input
              type="text"
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              placeholder={placeholder}
              className="w-full rounded-xl border border-zinc-800 bg-zinc-950 py-4 pl-12 pr-12 text-white placeholder-zinc-500 transition-all focus:border-orange-500/50 focus:outline-none focus:ring-2 focus:ring-orange-500/20"
            />
            {searchQuery && (
              <button
                onClick={handleClear}
                className="absolute right-4 top-1/2 -translate-y-1/2 rounded-lg p-1 text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-white"
                aria-label="Clear search"
              >
                <X className="h-5 w-5" />
              </button>
            )}
          </div>
 
          {/* Search Results Count */}
          {searchQuery && (
            <div className="mt-3 text-sm text-zinc-500">
              {filteredFAQs.length === 0 ? (
                <span>No results found for "{searchQuery}"</span>
              ) : (
                <span>
                  Found {filteredFAQs.length}{" "}
                  {filteredFAQs.length === 1 ? "result" : "results"}
                </span>
              )}
            </div>
          )}
        </div>
 
        {/* FAQs */}
        {filteredFAQs.length > 0 ? (
          <Accordion type="single" collapsible className="space-y-3">
            {filteredFAQs.map((faq) => (
              <AccordionItem
                key={faq.id}
                value={faq.id}
                className="rounded-xl border border-zinc-800 bg-zinc-950 px-6 data-[state=open]:border-orange-500/50"
              >
                <AccordionTrigger className="text-left text-base font-semibold text-white hover:text-orange-500 hover:no-underline">
                  <div className="flex flex-col items-start gap-2">
                    <span>{faq.question}</span>
                    {faq.category && (
                      <span className="rounded-full bg-orange-500/10 px-3 py-1 text-xs font-medium text-orange-500">
                        {faq.category}
                      </span>
                    )}
                  </div>
                </AccordionTrigger>
                <AccordionContent className="text-zinc-400">
                  {faq.answer}
                </AccordionContent>
              </AccordionItem>
            ))}
          </Accordion>
        ) : (
          <div className="rounded-xl border border-zinc-800 bg-zinc-950 p-12 text-center">
            <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-zinc-900">
              <Search className="h-8 w-8 text-zinc-600" />
            </div>
            <h3 className="mb-2 text-xl font-semibold text-white">
              No results found
            </h3>
            <p className="text-zinc-500">
              Try adjusting your search terms or browse all questions below
            </p>
            <button
              onClick={handleClear}
              className="mt-4 text-sm text-orange-500 hover:text-orange-400"
            >
              Clear search
            </button>
          </div>
        )}
 
        {/* CTA */}
        <div className="mt-12 rounded-2xl border border-zinc-800 bg-zinc-950 p-8 text-center">
          <h3 className="mb-2 text-xl font-bold text-white">
            Can't find what you're looking for?
          </h3>
          <p className="mb-6 text-zinc-400">
            Our support team is ready to help you with any questions
          </p>
          <a
            href="#contact"
            className="inline-flex items-center gap-2 rounded-lg bg-orange-600 px-6 py-3 font-semibold text-white transition-all hover:bg-orange-700"
          >
            Contact Support
          </a>
        </div>
      </div>
    </section>
  );
}

install the dependencies.

npx shadcn@latest add accordion
or 
pnpm dlx shadcn@latest add accordion

Features

  • Real-time search - Instant filtering as you type
  • Multi-field search - Searches questions, answers, and categories
  • Category badges - Visual category indicators
  • Result count - Shows number of matching results
  • Clear button - Quick search reset
  • Empty state - Helpful message when no results found
  • Accordion functionality - Expand/collapse answers
  • Hover effects - Interactive orange accent
  • Focus states - Orange ring on search input
  • TypeScript support - Full type safety

Usage

Basic Usage

import FAQSearch from "@/components/ui/faq-search";
 
const faqs = [
  {
    id: "1",
    question: "What is your refund policy?",
    answer: "We offer a 30-day money-back guarantee...",
    category: "Billing",
  },
  {
    id: "2",
    question: "How do I get started?",
    answer: "Getting started is easy...",
    category: "Getting Started",
  },
  // ... more FAQs
];
 
export default function Page() {
  return <FAQSearch faqs={faqs} />;
}

Custom Title and Description

<FAQSearch
  title="Help Center"
  description="Find answers to your questions instantly"
  faqs={faqs}
/>

Custom Placeholder

<FAQSearch
  faqs={faqs}
  placeholder="Type your question here..."
/>

Without Categories

const faqs = [
  {
    id: "1",
    question: "Question here?",
    answer: "Answer here",
    // No category field
  },
];
 
<FAQSearch faqs={faqs} />

Props

FAQSearchProps

PropTypeDefaultDescription
faqsFAQItem[]RequiredArray of FAQ objects
titlestring"Frequently Asked Questions"Section title
descriptionstring"Search through our knowledge base..."Section description
placeholderstring"Search for questions..."Search input placeholder
classNamestring""Additional CSS classes

FAQItem

PropTypeRequiredDescription
idstringYesUnique identifier
questionstringYesFAQ question
answerstringYesFAQ answer
categorystringNoCategory badge label

TypeScript Interface

interface FAQItem {
  id: string;
  question: string;
  answer: string;
  category?: string;
}
 
interface FAQSearchProps {
  title?: string;
  description?: string;
  faqs: FAQItem[];
  placeholder?: string;
  className?: string;
}

Search Behavior

  • Real-time filtering - Results update as you type
  • Case-insensitive - Matches regardless of case
  • Multi-field - Searches question, answer, and category
  • Partial matching - Finds results containing search terms
  • Instant results - No delay or debouncing
  • Clear button - Appears when search has text

Styling

  • Search bar - Zinc-950 with orange focus ring
  • Borders - Zinc-800 default, orange-500/50 when open
  • Text - White questions, zinc-400 answers
  • Category badges - Orange-500/10 background with orange-500 text
  • Icons - Search and clear icons in zinc-500
  • Empty state - Centered with icon and helpful message
  • CTA Card - Zinc-950 with orange button

Empty State

When no results are found:

  • Shows search icon in gray circle
  • Displays "No results found" heading
  • Provides helpful message
  • Offers "Clear search" button
  • Suggests browsing all questions

Result Count

  • Shows when search is active
  • Displays number of matching results
  • Updates in real-time
  • Singular/plural handling ("1 result" vs "2 results")
  • Shows "No results found" message when zero

Category Badges

  • Optional category labels
  • Orange color theme
  • Displayed below question
  • Included in search filtering
  • Rounded pill design

Responsive Behavior

  • Desktop - Full-width search bar
  • Tablet - Maintains layout
  • Mobile - Stacks naturally
  • Touch-friendly - Large tap targets

Use Cases

Perfect for:

  • Knowledge bases
  • Help centers
  • Documentation sites
  • Support pages
  • FAQ sections
  • Customer portals
  • Product help
  • Service pages

Best Practices

  1. Number of FAQs - Works best with 10+ questions
  2. Categories - Use consistent category names
  3. Content - Include keywords in questions and answers
  4. Order - Place most common questions first
  5. Updates - Keep content current and searchable
  6. Testing - Test search with common terms
  7. Feedback - Monitor what users search for
  8. Alternatives - Provide contact option when no results

Customization

Add Search Analytics

const [searchQuery, setSearchQuery] = useState("");
 
const handleSearch = (value: string) => {
  setSearchQuery(value);
  // Track search analytics
  analytics.track("FAQ Search", { query: value });
};

Highlight Search Terms

const highlightText = (text: string, query: string) => {
  if (!query) return text;
  const parts = text.split(new RegExp(`(${query})`, "gi"));
  return parts.map((part, i) =>
    part.toLowerCase() === query.toLowerCase() ? (
      <mark key={i} className="bg-orange-500/20 text-orange-500">
        {part}
      </mark>
    ) : (
      part
    )
  );
};
<div className="mb-4 flex flex-wrap gap-2">
  <span className="text-sm text-zinc-500">Popular:</span>
  {["refund", "pricing", "security"].map((term) => (
    <button
      key={term}
      onClick={() => setSearchQuery(term)}
      className="rounded-full bg-zinc-900 px-3 py-1 text-sm text-zinc-400 hover:bg-zinc-800"
    >
      {term}
    </button>
  ))}
</div>

Add Keyboard Shortcuts

useEffect(() => {
  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === "/" && e.metaKey) {
      e.preventDefault();
      searchInputRef.current?.focus();
    }
  };
  window.addEventListener("keydown", handleKeyDown);
  return () => window.removeEventListener("keydown", handleKeyDown);
}, []);