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>
);
}