Retour au catalogue

FAQ Accordion

FAQ classique en accordion avec animation d'ouverture/fermeture. Un seul item ouvert a la fois.

faqsimple Both Responsive a11y
minimalcorporatesaasagencyuniversalstacked
Theme
"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Plus } from "lucide-react";

interface FaqItem {
  question: string;
  answer: string;
}

interface FaqAccordionProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  items?: FaqItem[];
}

const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];

export default function FaqAccordion({
  badge = "FAQ",
  title = "Questions frequentes",
  subtitle = "",
  items = [],
}: FaqAccordionProps) {
  const [openIndex, setOpenIndex] = useState<number | null>(null);

  return (
    <section
      className="py-20 lg:py-28"
      style={{ background: "var(--color-background)" }}
    >
      <div className="mx-auto max-w-3xl px-6">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, ease }}
          viewport={{ once: true }}
          className="text-center mb-14"
        >
          {badge && (
            <span
              className="inline-block mb-4 text-xs font-medium tracking-widest uppercase"
              style={{ color: "var(--color-accent)" }}
            >
              {badge}
            </span>
          )}
          <h2
            className="text-3xl md:text-4xl lg:text-5xl font-bold"
            style={{ color: "var(--color-foreground)" }}
          >
            {title}
          </h2>
          {subtitle && (
            <p
              className="mt-3 text-base"
              style={{ color: "var(--color-foreground-muted)" }}
            >
              {subtitle}
            </p>
          )}
        </motion.div>

        <div className="flex flex-col">
          {items.map((item, i) => {
            const isOpen = openIndex === i;
            return (
              <motion.div
                key={i}
                initial={{ opacity: 0, y: 10 }}
                whileInView={{ opacity: 1, y: 0 }}
                transition={{ duration: 0.4, ease, delay: i * 0.04 }}
                viewport={{ once: true }}
                style={{ borderBottom: "1px solid var(--color-border)" }}
              >
                <button
                  onClick={() => setOpenIndex(isOpen ? null : i)}
                  className="w-full flex items-center justify-between gap-4 py-5 text-left"
                >
                  <span
                    className="text-base font-medium"
                    style={{
                      color: isOpen
                        ? "var(--color-foreground)"
                        : "var(--color-foreground-muted)",
                      transition: "color 0.2s",
                    }}
                  >
                    {item.question}
                  </span>
                  <motion.div
                    animate={{ rotate: isOpen ? 45 : 0 }}
                    transition={{ duration: 0.2 }}
                    className="flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center"
                    style={{
                      background: isOpen
                        ? "var(--color-accent)"
                        : "var(--color-background-alt)",
                      transition: "background 0.2s",
                    }}
                  >
                    <Plus
                      size={14}
                      style={{
                        color: isOpen
                          ? "var(--color-background)"
                          : "var(--color-foreground-muted)",
                      }}
                    />
                  </motion.div>
                </button>
                <AnimatePresence initial={false}>
                  {isOpen && (
                    <motion.div
                      initial={{ height: 0, opacity: 0 }}
                      animate={{ height: "auto", opacity: 1 }}
                      exit={{ height: 0, opacity: 0 }}
                      transition={{ duration: 0.3, ease }}
                      className="overflow-hidden"
                    >
                      <p
                        className="pb-5 text-sm leading-relaxed"
                        style={{ color: "var(--color-foreground-muted)" }}
                      >
                        {item.answer}
                      </p>
                    </motion.div>
                  )}
                </AnimatePresence>
              </motion.div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Avis

FAQ Accordion — React Faq Section — Incubator