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

Avis

Split Content Accordion — React Split-content Section — Incubator