Retour au catalogue
Split Content Accordion
Split layout avec texte et image a gauche, accordion de details a droite. Chaque item s'expand avec animation height + contenu.
split-contentmedium Both Responsive a11y
minimalcorporateuniversalsaasagencysplit
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import * as LucideIcons from "lucide-react";
import { ChevronDown } from "lucide-react";
import React from "react";
interface AccordionItem {
id: string;
title: string;
content: string;
icon?: string;
}
interface SplitContentAccordionProps {
badge?: string;
title?: string;
description?: string;
items?: AccordionItem[];
}
const EASE = [0.16, 1, 0.3, 1] as const;
function getIcon(name?: string) {
if (!name) return null;
return (LucideIcons as unknown as Record<string, React.ElementType>)[name] || null;
}
export default function SplitContentAccordion({
badge = "En detail",
title = "Tout ce que vous devez savoir",
description = "Des reponses claires a vos questions.",
items = [],
}: SplitContentAccordionProps) {
const [openId, setOpenId] = useState<string | null>(items[0]?.id ?? null);
return (
<section
style={{ padding: "var(--section-padding-y-lg) 0", background: "var(--color-background)" }}
>
<div
style={{
maxWidth: "var(--container-max-width)",
margin: "0 auto",
padding: "0 var(--container-padding-x)",
}}
>
<div
style={{ display: "grid", gridTemplateColumns: "1fr", gap: "3rem", alignItems: "center" }}
className="md:!grid-cols-2"
>
{/* Left: text */}
<motion.div
initial={{ opacity: 0, x: -24 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: EASE }}
>
{badge && (
<span
style={{
display: "inline-block",
fontSize: "0.75rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.08em",
color: "var(--color-accent)",
marginBottom: "0.75rem",
}}
>
{badge}
</span>
)}
<h2
style={{
fontFamily: "var(--font-sans)",
fontSize: "clamp(1.75rem, 3vw, 2.5rem)",
fontWeight: 800,
letterSpacing: "-0.03em",
color: "var(--color-foreground)",
marginBottom: "1rem",
}}
>
{title}
</h2>
<p
style={{
fontSize: "1.0625rem",
lineHeight: 1.7,
color: "var(--color-foreground-muted)",
maxWidth: "460px",
}}
>
{description}
</p>
</motion.div>
{/* Right: accordion */}
<motion.div
initial={{ opacity: 0, x: 24 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1, ease: EASE }}
style={{ display: "flex", flexDirection: "column", gap: "0" }}
>
{items.map((item) => {
const isOpen = openId === item.id;
const Icon = getIcon(item.icon);
return (
<div
key={item.id}
style={{ borderBottom: "1px solid var(--color-border)" }}
>
<button
onClick={() => setOpenId(isOpen ? null : item.id)}
style={{
width: "100%",
display: "flex",
alignItems: "center",
gap: "0.75rem",
padding: "1.25rem 0",
background: "none",
border: "none",
cursor: "pointer",
textAlign: "left",
}}
>
{Icon && (
<span
style={{
width: "36px",
height: "36px",
borderRadius: "var(--radius-md)",
background: isOpen
? "color-mix(in srgb, var(--color-accent) 15%, transparent)"
: "var(--color-background-alt)",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
transition: "background 0.3s ease",
}}
>
<Icon
style={{
width: 16,
height: 16,
color: isOpen ? "var(--color-accent)" : "var(--color-foreground-muted)",
}}
/>
</span>
)}
<span
style={{
flex: 1,
fontSize: "1rem",
fontWeight: 600,
color: "var(--color-foreground)",
}}
>
{item.title}
</span>
<motion.span
animate={{ rotate: isOpen ? 180 : 0 }}
transition={{ duration: 0.3 }}
>
<ChevronDown
style={{ width: 18, height: 18, color: "var(--color-foreground-muted)" }}
/>
</motion.span>
</button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: EASE }}
style={{ overflow: "hidden" }}
>
<p
style={{
padding: "0 0 1.25rem 3rem",
fontSize: "0.9375rem",
lineHeight: 1.7,
color: "var(--color-foreground-muted)",
}}
>
{item.content}
</p>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
</motion.div>
</div>
</div>
</section>
);
}