# Faq 3

> Searchable FAQ section with filter animations

A shadcn/ui block from the [@smoothui](https://smoothui.dev) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/smoothui/faq-3).

- Page, with a live preview: https://shadcn.exchange/r/smoothui/faq-3
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/smoothui/faq-3-light-0b2c6f65ae1a8b4f.png
- Registry item: https://smoothui.dev/r/faq-3.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://smoothui.dev/r/faq-3.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `https://smoothui.dev/r/tokens.json`

## Files

### `index.tsx`

```tsx
"use client";

import { ChevronDown, Search } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useMemo, useState } from "react";

export interface FaqSearchableProps {
  description?: string;
  faqs?: Array<{
    question: string;
    answer: string;
  }>;
  noResultsText?: string;
  searchPlaceholder?: string;
  title?: string;
}

const defaultFaqs = [
  {
    answer:
      "Getting started is easy! Simply install the package via npm or pnpm, import the components you need, and start building. We provide comprehensive documentation and examples to help you get up and running quickly.",
    question: "How do I get started with SmoothUI?",
  },
  {
    answer:
      "Yes! SmoothUI is completely free and open source. You can use it in both personal and commercial projects without any restrictions. We believe in making beautiful UI components accessible to everyone.",
    question: "Is SmoothUI free to use?",
  },
  {
    answer:
      "SmoothUI requires React 18 or later and works with Next.js 13+. You'll also need Node.js 18+ and a modern browser that supports CSS animations and transforms.",
    question: "What are the system requirements?",
  },
  {
    answer:
      "Absolutely! All animations are fully customizable. You can modify timing, easing, and effects using Motion (Framer Motion) props. We also respect the prefers-reduced-motion setting for accessibility.",
    question: "Can I customize the animations?",
  },
  {
    answer:
      "You can report bugs by opening an issue on our GitHub repository. Please include a detailed description, steps to reproduce, and your environment details. We actively monitor and respond to issues.",
    question: "How do I report bugs or issues?",
  },
  {
    answer:
      "Yes, we offer enterprise support packages that include priority bug fixes, dedicated support channels, custom component development, and SLA guarantees. Contact us for more information.",
    question: "Is there enterprise support available?",
  },
];

export function FaqSearchable({
  title = "Frequently Asked Questions",
  description = "Search through our FAQ to find answers to your questions",
  searchPlaceholder = "Search questions...",
  noResultsText = "No matching questions found. Try a different search term.",
  faqs = defaultFaqs,
}: FaqSearchableProps) {
  const [searchQuery, setSearchQuery] = useState("");
  const [openIndex, setOpenIndex] = useState<number | null>(null);
  const shouldReduceMotion = useReducedMotion();

  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)
    );
  }, [faqs, searchQuery]);

  const toggleAccordion = (index: number) => {
    setOpenIndex(openIndex === index ? null : index);
  };

  const springTransition = shouldReduceMotion
    ? { duration: 0 }
    : { bounce: 0.05, duration: 0.25, type: "spring" as const };

  const contentTransition = shouldReduceMotion
    ? { duration: 0 }
    : { bounce: 0, duration: 0.25, type: "spring" as const };

  return (
    <section className="py-20">
      <div className="mx-auto max-w-4xl px-6">
        <motion.div
          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
          className="mb-12 text-center"
          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}
          transition={springTransition}
        >
          <h2 className="mb-4 font-bold text-3xl text-foreground lg:text-4xl">
            {title}
          </h2>
          <p className="mx-auto max-w-2xl text-foreground/70 text-lg">
            {description}
          </p>
        </motion.div>

        <motion.div
          animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
          className="relative mb-8"
          initial={shouldReduceMotion ? { opacity: 1 } : { opacity: 0, y: 20 }}
          transition={{
            ...springTransition,
            delay: shouldReduceMotion ? 0 : 0.1,
          }}
        >
          <Search className="absolute top-1/2 left-4 h-5 w-5 -translate-y-1/2 text-foreground/40" />
          <input
            aria-label="Search frequently asked questions"
            className="w-full rounded-xl border border-border bg-background py-4 pr-4 pl-12 text-foreground transition-colors placeholder:text-foreground/40 focus:border-brand focus:outline-none focus:ring-2 focus:ring-brand/20"
            onChange={(e) => {
              setSearchQuery(e.target.value);
              setOpenIndex(null);
            }}
            placeholder={searchPlaceholder}
            type="text"
            value={searchQuery}
          />
        </motion.div>

        <div className="space-y-4">
          <AnimatePresence mode="popLayout">
            {filteredFaqs.length === 0 ? (
              <motion.div
                animate={
                  shouldReduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1 }
                }
                className="rounded-xl border border-border bg-background/50 py-12 text-center"
                exit={
                  shouldReduceMotion
                    ? { opacity: 0, transition: { duration: 0 } }
                    : { opacity: 0, scale: 0.95 }
                }
                initial={
                  shouldReduceMotion
                    ? { opacity: 1 }
                    : { opacity: 0, scale: 0.95 }
                }
                key="no-results"
                transition={springTransition}
              >
                <p className="text-foreground/60">{noResultsText}</p>
              </motion.div>
            ) : (
              filteredFaqs.map((faq, index) => {
                const originalIndex = faqs.indexOf(faq);
                const isOpen = openIndex === originalIndex;

                return (
                  <motion.div
                    animate={
                      shouldReduceMotion
                        ? { opacity: 1 }
                        : { opacity: 1, scale: 1, y: 0 }
                    }
                    className="group overflow-hidden rounded-xl border border-border bg-background transition-colors hover:border-brand"
                    exit={
                      shouldReduceMotion
                        ? { opacity: 0, transition: { duration: 0 } }
                        : { opacity: 0, scale: 0.95, y: -10 }
                    }
                    initial={
                      shouldReduceMotion
                        ? { opacity: 1 }
                        : { opacity: 0, scale: 0.95, y: 20 }
                    }
                    key={faq.question}
                    layout={!shouldReduceMotion}
                    transition={{
                      ...springTransition,
                      delay: shouldReduceMotion ? 0 : index * 0.05,
                    }}
                  >
                    <button
                      aria-expanded={isOpen}
                      className="flex w-full cursor-pointer items-center justify-between p-5 text-left transition-colors hover:bg-background/50"
                      onClick={() => toggleAccordion(originalIndex)}
                      type="button"
                    >
                      <h3 className="pr-4 font-medium text-foreground">
                        {faq.question}
                      </h3>
                      <motion.div
                        animate={{
                          rotate: isOpen ? 180 : 0,
                        }}
                        className="flex-shrink-0"
                        transition={springTransition}
                      >
                        <ChevronDown
                          aria-hidden="true"
                          className="h-5 w-5 text-foreground/60"
                        />
                      </motion.div>
                    </button>

                    <AnimatePresence>
                      {isOpen && (
                        <motion.div
                          animate={
                            shouldReduceMotion
                              ? { height: "auto", opacity: 1 }
                              : { height: "auto", opacity: 1 }
                          }
                          className="overflow-hidden"
                          exit={
                            shouldReduceMotion
                              ? {
                                  height: 0,
                                  opacity: 0,
                                  transition: { duration: 0 },
                                }
                              : { height: 0, opacity: 0 }
                          }
                          initial={
                            shouldReduceMotion
                              ? { height: "auto", opacity: 1 }
                              : { height: 0, opacity: 0 }
                          }
                          transition={contentTransition}
                        >
                          <div className="px-5 pb-5">
                            <p className="text-foreground/70 leading-relaxed">
                              {faq.answer}
                            </p>
                          </div>
                        </motion.div>
                      )}
                    </AnimatePresence>
                  </motion.div>
                );
              })
            )}
          </AnimatePresence>
        </div>
      </div>
    </section>
  );
}

export default FaqSearchable;
```
