Retour au catalogue

Content Tabs Pill

Tabs en pill/segment avec animation de slide, contenu texte avec stats et visuel a droite.

content-tabsmedium Both Responsive a11y
minimalplayfuluniversalsaasagencycentered
Theme
"use client";

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

interface TabItem {
  id: string;
  label: string;
  icon?: string;
  title: string;
  description: string;
  stats?: { label: string; value: string }[];
}

interface ContentTabsPillProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  tabs: TabItem[];
}

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 ContentTabsPill({
  badge = "Solutions",
  title = "Adaptee a chaque besoin",
  subtitle = "Selectionnez votre profil pour decouvrir la solution ideale.",
  tabs = [],
}: ContentTabsPillProps) {
  const [activeTab, setActiveTab] = useState(tabs[0]?.id ?? "");

  const current = tabs.find((t) => t.id === activeTab);

  return (
    <section
      style={{
        padding: "var(--section-padding-y, 6rem) 0",
        background: "var(--color-background-alt)",
      }}
    >
      <div
        style={{
          maxWidth: "var(--container-max-width, 1200px)",
          margin: "0 auto",
          padding: "0 var(--container-padding-x, 1.5rem)",
        }}
      >
        {/* Header */}
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-80px" }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{ textAlign: "center", maxWidth: "600px", margin: "0 auto 2.5rem" }}
        >
          {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.75rem)",
              fontWeight: 700,
              lineHeight: 1.15,
              letterSpacing: "-0.02em",
              color: "var(--color-foreground)",
              marginBottom: "1rem",
            }}
          >
            {title}
          </h2>
          <p style={{ fontSize: "1rem", lineHeight: 1.7, color: "var(--color-foreground-muted)" }}>
            {subtitle}
          </p>
        </motion.div>

        {/* Pill tab bar */}
        <div
          style={{
            display: "flex",
            justifyContent: "center",
            marginBottom: "3rem",
          }}
        >
          <div
            style={{
              display: "inline-flex",
              gap: "4px",
              padding: "4px",
              borderRadius: "var(--radius-full)",
              background: "var(--color-background)",
              border: "1px solid var(--color-border)",
            }}
          >
            {tabs.map((tab) => {
              const Icon = getIcon(tab.icon);
              const isActive = tab.id === activeTab;
              return (
                <button
                  key={tab.id}
                  onClick={() => setActiveTab(tab.id)}
                  style={{
                    position: "relative",
                    display: "flex",
                    alignItems: "center",
                    gap: "6px",
                    padding: "0.625rem 1.25rem",
                    fontSize: "0.875rem",
                    fontWeight: 500,
                    color: isActive ? "var(--color-foreground)" : "var(--color-foreground-muted)",
                    background: "none",
                    border: "none",
                    borderRadius: "var(--radius-full)",
                    cursor: "pointer",
                    zIndex: 1,
                    whiteSpace: "nowrap",
                    transition: "color var(--duration-fast) var(--ease-out)",
                  }}
                >
                  {isActive && (
                    <motion.div
                      layoutId="pill-indicator"
                      style={{
                        position: "absolute",
                        inset: 0,
                        borderRadius: "var(--radius-full)",
                        background: "var(--color-accent)",
                        opacity: 0.12,
                      }}
                      transition={{ type: "spring", stiffness: 400, damping: 30 }}
                    />
                  )}
                  {Icon && <Icon style={{ width: 16, height: 16, position: "relative", zIndex: 1 }} />}
                  <span style={{ position: "relative", zIndex: 1 }}>{tab.label}</span>
                </button>
              );
            })}
          </div>
        </div>

        {/* Tab content */}
        <AnimatePresence mode="wait">
          {current && (
            <motion.div
              key={current.id}
              initial={{ opacity: 0, y: 16 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: -10 }}
              transition={{ duration: 0.35, ease: EASE }}
              style={{
                display: "grid",
                gap: "2rem",
                alignItems: "center",
              }}
              className="md:grid-cols-[1fr_1.2fr]"
            >
              <div>
                <h3
                  style={{
                    fontFamily: "var(--font-sans)",
                    fontSize: "1.5rem",
                    fontWeight: 700,
                    color: "var(--color-foreground)",
                    marginBottom: "1rem",
                  }}
                >
                  {current.title}
                </h3>
                <p
                  style={{
                    fontSize: "1rem",
                    lineHeight: 1.7,
                    color: "var(--color-foreground-muted)",
                    marginBottom: current.stats?.length ? "2rem" : "0",
                  }}
                >
                  {current.description}
                </p>

                {current.stats && current.stats.length > 0 && (
                  <div style={{ display: "flex", gap: "2rem", flexWrap: "wrap" }}>
                    {current.stats.map((stat, i) => (
                      <div key={i}>
                        <p
                          style={{
                            fontSize: "1.75rem",
                            fontWeight: 800,
                            color: "var(--color-accent)",
                            lineHeight: 1.2,
                          }}
                        >
                          {stat.value}
                        </p>
                        <p
                          style={{
                            fontSize: "0.8125rem",
                            color: "var(--color-foreground-muted)",
                            marginTop: "0.25rem",
                          }}
                        >
                          {stat.label}
                        </p>
                      </div>
                    ))}
                  </div>
                )}
              </div>

              {/* Visual placeholder */}
              <div
                style={{
                  aspectRatio: "4/3",
                  borderRadius: "var(--radius-xl, 1.5rem)",
                  background: "var(--color-background)",
                  border: "1px solid var(--color-border)",
                }}
              />
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </section>
  );
}

Avis

Content Tabs Pill — React Content-tabs Section — Incubator