Retour au catalogue

Services Accordion

Services en accordeon avec animation d'expansion. Design epure et compact.

servicesmedium Both Responsive a11y
corporateelegantuniversallegalmedicalstacked
Theme
"use client";

import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import * as LucideIcons from "lucide-react";
import { ChevronDown } from "lucide-react";

interface ServiceItem {
  id: string;
  title: string;
  icon?: string;
  description: string;
  deliverables?: string[];
}

interface ServicesAccordionProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  services: ServiceItem[];
}

function getIcon(name?: string) {
  if (!name) return null;
  return (LucideIcons as unknown as Record<string, React.ElementType>)[name] || null;
}

export default function ServicesAccordion({
  badge,
  title,
  subtitle,
  services,
}: ServicesAccordionProps) {
  const [openId, setOpenId] = useState<string | null>(services[0]?.id ?? null);

  return (
    <section
      className="py-[var(--section-padding-y,6rem)]"
      style={{ backgroundColor: "var(--color-background)" }}
    >
      <div className="mx-auto max-w-3xl px-[var(--container-padding-x,1.5rem)]">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-80px" }}
          transition={{ duration: 0.5 }}
          className="text-center"
        >
          {badge && (
            <span
              className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border"
              style={{
                color: "var(--color-accent)",
                borderColor: "var(--color-border)",
              }}
            >
              {badge}
            </span>
          )}
          {title && (
            <h2
              className="mt-4 text-3xl font-bold tracking-tight md:text-4xl"
              style={{ color: "var(--color-foreground)" }}
            >
              {title}
            </h2>
          )}
          {subtitle && (
            <p
              className="mt-3 text-sm"
              style={{ color: "var(--color-foreground-muted)" }}
            >
              {subtitle}
            </p>
          )}
        </motion.div>

        <div className="mt-12 flex flex-col gap-3">
          {services.map((service, i) => {
            const isOpen = openId === service.id;
            const Icon = getIcon(service.icon);
            return (
              <motion.div
                key={service.id}
                initial={{ opacity: 0, y: 16 }}
                whileInView={{ opacity: 1, y: 0 }}
                viewport={{ once: true, margin: "-40px" }}
                transition={{ delay: i * 0.06, duration: 0.4 }}
                className="rounded-[var(--radius-lg,1rem)] border overflow-hidden"
                style={{
                  borderColor: isOpen ? "var(--color-accent)" : "var(--color-border)",
                  backgroundColor: "var(--color-background-card)",
                }}
              >
                <button
                  onClick={() => setOpenId(isOpen ? null : service.id)}
                  className="flex w-full items-center gap-4 px-6 py-5 text-left transition-colors"
                  aria-expanded={isOpen}
                >
                  {Icon && (
                    <Icon
                      className="h-5 w-5 shrink-0"
                      style={{ color: "var(--color-accent)" }}
                    />
                  )}
                  <span
                    className="flex-1 text-base font-semibold"
                    style={{ color: "var(--color-foreground)" }}
                  >
                    {service.title}
                  </span>
                  <ChevronDown
                    className="h-5 w-5 shrink-0 transition-transform duration-300"
                    style={{
                      color: "var(--color-foreground-muted)",
                      transform: isOpen ? "rotate(180deg)" : "rotate(0deg)",
                    }}
                  />
                </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: [0.16, 1, 0.3, 1] }}
                      className="overflow-hidden"
                    >
                      <div
                        className="px-6 pb-6 border-t pt-4"
                        style={{ borderColor: "var(--color-border)" }}
                      >
                        <p
                          className="text-sm leading-relaxed"
                          style={{ color: "var(--color-foreground-muted)" }}
                        >
                          {service.description}
                        </p>
                        {service.deliverables && service.deliverables.length > 0 && (
                          <div className="mt-4 flex flex-wrap gap-2">
                            {service.deliverables.map((d) => (
                              <span
                                key={d}
                                className="text-xs font-medium px-3 py-1 rounded-full"
                                style={{
                                  backgroundColor: "color-mix(in srgb, var(--color-accent) 12%, transparent)",
                                  color: "var(--color-accent)",
                                }}
                              >
                                {d}
                              </span>
                            ))}
                          </div>
                        )}
                      </div>
                    </motion.div>
                  )}
                </AnimatePresence>
              </motion.div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Avis

Services Accordion — React Services Section — Incubator