Retour au catalogue

Features Animated Reveal

Features qui se revelent une par une au scroll avec stagger alterne gauche/droite.

featuresmedium Both Responsive a11y
elegantcorporateuniversalsaasagencystacked
Theme
"use client";

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

interface FeatureItem {
  id: string;
  title: string;
  description: string;
  icon?: string;
}

interface FeaturesAnimatedRevealProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  features: FeatureItem[];
}

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

const EASE = [0.16, 1, 0.3, 1] as const;

export default function FeaturesAnimatedReveal({
  badge,
  title,
  subtitle,
  features,
}: FeaturesAnimatedRevealProps) {
  return (
    <section
      style={{
        padding: "var(--section-padding-y, 6rem) 0",
        background: "var(--color-background)",
      }}
    >
      <div
        style={{
          maxWidth: "var(--container-max-width)",
          margin: "0 auto",
          padding: "0 var(--container-padding-x)",
        }}
      >
        {/* Header */}
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-60px" }}
          transition={{ duration: 0.5, ease: EASE }}
          style={{ textAlign: "center", maxWidth: 600, margin: "0 auto 4rem" }}
        >
          {badge && (
            <span
              style={{
                display: "inline-block",
                fontSize: "0.75rem",
                fontWeight: 600,
                textTransform: "uppercase",
                letterSpacing: "0.08em",
                color: "var(--color-accent)",
                marginBottom: "1rem",
              }}
            >
              {badge}
            </span>
          )}
          {title && (
            <h2
              style={{
                fontFamily: "var(--font-sans)",
                fontSize: "clamp(1.75rem, 3vw, 2.75rem)",
                fontWeight: 700,
                letterSpacing: "-0.02em",
                lineHeight: 1.15,
                color: "var(--color-foreground)",
                marginBottom: "1rem",
              }}
            >
              {title}
            </h2>
          )}
          {subtitle && (
            <p
              style={{
                fontSize: "1.0625rem",
                lineHeight: 1.7,
                color: "var(--color-foreground-muted)",
              }}
            >
              {subtitle}
            </p>
          )}
        </motion.div>

        {/* Staggered reveal list */}
        <div
          style={{
            display: "flex",
            flexDirection: "column",
            gap: "1.5rem",
            maxWidth: 720,
            margin: "0 auto",
          }}
        >
          {features.map((feature, i) => {
            const Icon = getIcon(feature.icon);
            return (
              <motion.div
                key={feature.id}
                initial={{ opacity: 0, x: i % 2 === 0 ? -30 : 30 }}
                whileInView={{ opacity: 1, x: 0 }}
                viewport={{ once: true, margin: "-40px" }}
                transition={{ delay: i * 0.1, duration: 0.6, ease: EASE }}
                style={{
                  display: "flex",
                  alignItems: "flex-start",
                  gap: "1.25rem",
                  padding: "1.5rem",
                  borderRadius: "var(--radius-lg, 1rem)",
                  border: "1px solid var(--color-border)",
                  background: "var(--color-background-card, var(--color-background))",
                  transition: "border-color var(--duration-normal) var(--ease-out)",
                }}
              >
                {Icon && (
                  <div
                    style={{
                      flexShrink: 0,
                      display: "flex",
                      alignItems: "center",
                      justifyContent: "center",
                      width: 44,
                      height: 44,
                      borderRadius: "var(--radius-md, 0.75rem)",
                      background:
                        "color-mix(in srgb, var(--color-accent) 10%, transparent)",
                    }}
                  >
                    <Icon
                      style={{
                        width: 20,
                        height: 20,
                        color: "var(--color-accent)",
                      }}
                    />
                  </div>
                )}
                <div>
                  <h3
                    style={{
                      fontFamily: "var(--font-sans)",
                      fontSize: "1rem",
                      fontWeight: 600,
                      color: "var(--color-foreground)",
                      marginBottom: "0.375rem",
                    }}
                  >
                    {feature.title}
                  </h3>
                  <p
                    style={{
                      fontSize: "0.9375rem",
                      lineHeight: 1.6,
                      color: "var(--color-foreground-muted)",
                      margin: 0,
                    }}
                  >
                    {feature.description}
                  </p>
                </div>
              </motion.div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Avis

Features Animated Reveal — React Features Section — Incubator