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 SupportInstallation
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 accordionFeatures
- ✅ 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
| Prop | Type | Default | Description |
|---|---|---|---|
faqs | FAQItem[] | Required | Array of FAQ objects |
title | string | "Frequently Asked Questions" | Section title |
description | string | "Search through our knowledge base..." | Section description |
placeholder | string | "Search for questions..." | Search input placeholder |
className | string | "" | Additional CSS classes |
FAQItem
| Prop | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier |
question | string | Yes | FAQ question |
answer | string | Yes | FAQ answer |
category | string | No | Category 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
- Number of FAQs - Works best with 10+ questions
- Categories - Use consistent category names
- Content - Include keywords in questions and answers
- Order - Place most common questions first
- Updates - Keep content current and searchable
- Testing - Test search with common terms
- Feedback - Monitor what users search for
- 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
)
);
};Add Popular Searches
<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);
}, []);