Retour au catalogue

Mobile App Features Scroll

Presentation de fonctionnalites mobile liee au scroll avec telephone central qui change d'ecran a mesure que l'utilisateur scrolle.

mobile-appcomplex Both Responsive a11y
boldelegantsaasecommerceuniversalstacked
Theme
"use client";

import { motion, useInView } from "framer-motion";
import { Zap, Palette, Lock, Globe, Smartphone } from "lucide-react";
import { useRef, useState, useEffect } from "react";

interface Feature {
  icon: string;
  title: string;
  description: string;
  screenLabel: string;
}

interface MobileAppFeaturesScrollProps {
  title?: string;
  features?: Feature[];
}

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

const iconMap: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>> = {
  Zap,
  Palette,
  Lock,
  Globe,
  Smartphone,
};

function FeatureRow({
  feature,
  index,
  onVisible,
}: {
  feature: Feature;
  index: number;
  onVisible: (idx: number) => void;
}) {
  const ref = useRef<HTMLDivElement>(null);
  const isInView = useInView(ref, { amount: 0.6 });

  useEffect(() => {
    if (isInView) onVisible(index);
  }, [isInView, index, onVisible]);

  const Icon = iconMap[feature.icon] ?? Smartphone;
  const isEven = index % 2 === 0;

  return (
    <motion.div
      ref={ref}
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, amount: 0.4 }}
      transition={{ duration: 0.6, ease: EASE }}
      style={{
        display: "flex",
        flexDirection: isEven ? "row" : "row-reverse",
        alignItems: "center",
        gap: "3rem",
        padding: "4rem 0",
      }}
    >
      {/* Text */}
      <div style={{ flex: 1 }}>
        <div
          style={{
            width: 48,
            height: 48,
            borderRadius: "var(--radius-md)",
            background: "var(--color-accent-subtle)",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            marginBottom: "1rem",
          }}
        >
          <Icon style={{ width: 24, height: 24, color: "var(--color-accent)" }} />
        </div>
        <h3
          style={{
            fontFamily: "var(--font-sans)",
            fontSize: "clamp(1.25rem, 2vw, 1.75rem)",
            fontWeight: 700,
            color: "var(--color-foreground)",
            marginBottom: "0.75rem",
            letterSpacing: "-0.02em",
          }}
        >
          {feature.title}
        </h3>
        <p
          style={{
            fontSize: "1rem",
            lineHeight: 1.7,
            color: "var(--color-foreground-muted)",
            maxWidth: 400,
          }}
        >
          {feature.description}
        </p>
      </div>

      {/* Spacer for phone in center (handled separately) */}
      <div style={{ flex: 1 }} />
    </motion.div>
  );
}

export default function MobileAppFeaturesScroll({
  title = "Tout ce dont vous avez besoin",
  features = [],
}: MobileAppFeaturesScrollProps) {
  const [activeIndex, setActiveIndex] = useState(0);
  const containerRef = useRef<HTMLDivElement>(null);

  return (
    <section
      style={{
        padding: "var(--section-padding-y) 0",
        background: "var(--color-background)",
      }}
    >
      <div
        style={{
          maxWidth: "var(--container-max-width)",
          margin: "0 auto",
          padding: "0 var(--container-padding-x)",
        }}
      >
        {/* Title */}
        <motion.h2
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.6, ease: EASE }}
          style={{
            fontFamily: "var(--font-sans)",
            fontSize: "clamp(2rem, 4vw, 3rem)",
            fontWeight: 700,
            lineHeight: 1.1,
            letterSpacing: "-0.03em",
            color: "var(--color-foreground)",
            textAlign: "center",
            marginBottom: "4rem",
          }}
        >
          {title}
        </motion.h2>

        {/* Features with floating phone */}
        <div ref={containerRef} style={{ position: "relative" }}>
          {/* Sticky phone in the center */}
          <div
            style={{
              position: "sticky",
              top: "50%",
              transform: "translateY(-50%)",
              display: "flex",
              justifyContent: "center",
              pointerEvents: "none",
              zIndex: 2,
              height: 0,
            }}
          >
            <motion.div
              animate={{
                boxShadow:
                  activeIndex % 2 === 0
                    ? "0 30px 60px -15px rgba(0,0,0,0.15)"
                    : "0 30px 60px -15px rgba(0,0,0,0.25)",
              }}
              transition={{ duration: 0.5 }}
              style={{
                width: 200,
                height: 400,
                borderRadius: 32,
                border: "3px solid var(--color-foreground)",
                background: "var(--color-background-alt)",
                overflow: "hidden",
                position: "relative",
              }}
            >
              {/* Notch */}
              <div
                style={{
                  position: "absolute",
                  top: 0,
                  left: "50%",
                  transform: "translateX(-50%)",
                  width: 80,
                  height: 22,
                  borderRadius: "0 0 12px 12px",
                  background: "var(--color-foreground)",
                  zIndex: 3,
                }}
              />

              {/* Screen content */}
              <div
                style={{
                  height: "100%",
                  display: "flex",
                  flexDirection: "column",
                  alignItems: "center",
                  justifyContent: "center",
                  padding: "40px 16px 16px",
                }}
              >
                <motion.div
                  key={activeIndex}
                  initial={{ opacity: 0, scale: 0.9 }}
                  animate={{ opacity: 1, scale: 1 }}
                  transition={{ duration: 0.4, ease: EASE }}
                  style={{ textAlign: "center" }}
                >
                  {features[activeIndex] && (
                    <>
                      <div
                        style={{
                          width: 56,
                          height: 56,
                          borderRadius: "50%",
                          background: "var(--color-accent)",
                          opacity: 0.15,
                          margin: "0 auto 1rem",
                        }}
                      />
                      <div
                        style={{
                          fontSize: "0.8125rem",
                          fontWeight: 700,
                          color: "var(--color-foreground)",
                          fontFamily: "var(--font-sans)",
                          marginBottom: "0.5rem",
                        }}
                      >
                        {features[activeIndex].screenLabel}
                      </div>
                      <div
                        style={{
                          width: "80%",
                          margin: "0 auto",
                          display: "flex",
                          flexDirection: "column",
                          gap: 6,
                        }}
                      >
                        {[1, 0.7, 0.5].map((w, i) => (
                          <div
                            key={i}
                            style={{
                              height: 6,
                              borderRadius: 3,
                              background: "var(--color-border)",
                              width: `${w * 100}%`,
                            }}
                          />
                        ))}
                      </div>
                    </>
                  )}
                </motion.div>
              </div>

              {/* Home indicator */}
              <div
                style={{
                  position: "absolute",
                  bottom: 6,
                  left: "50%",
                  transform: "translateX(-50%)",
                  width: 60,
                  height: 3,
                  borderRadius: 2,
                  background: "var(--color-foreground)",
                  opacity: 0.25,
                }}
              />
            </motion.div>
          </div>

          {/* Feature rows */}
          {features.map((feature, i) => (
            <FeatureRow
              key={i}
              feature={feature}
              index={i}
              onVisible={setActiveIndex}
            />
          ))}
        </div>
      </div>
    </section>
  );
}

Avis

Mobile App Features Scroll — React Mobile-app Section — Incubator