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