Retour au catalogue
Early Access
Waitlist d'acces anticipe avec tiers de privileges, compteur d'inscrits anime et avantages exclusifs.
waitlistmedium Both Responsive a11y
elegantboldsaasecommercecentered
Theme
"use client";
import { useRef, useState } from "react";
import { motion, useInView, AnimatePresence } from "framer-motion";
import { Crown, ArrowRight, Check, Sparkles, Users, Star, Zap } from "lucide-react";
interface Tier {
name: string;
range: string;
perks: string[];
active: boolean;
}
interface SocialPerson {
initials: string;
name: string;
}
interface WaitlistEarlyAccessProps {
title?: string;
subtitle?: string;
placeholder?: string;
ctaLabel?: string;
badge?: string;
currentSignups?: number;
maxSignups?: number;
tiers?: Tier[];
socialProof?: SocialPerson[];
}
const EASE: [number, number, number, number] = [0.16, 1, 0.3, 1];
export default function WaitlistEarlyAccess({
title = "Acces anticipe exclusif",
subtitle = "Soyez parmi les premiers.",
placeholder = "votre@email.com",
ctaLabel = "Obtenir mon acces",
badge = "Places limitees",
currentSignups = 2847,
maxSignups = 5000,
tiers = [],
socialProof = [],
}: WaitlistEarlyAccessProps) {
const ref = useRef<HTMLDivElement>(null);
const inView = useInView(ref, { once: true, margin: "-80px" });
const [submitted, setSubmitted] = useState(false);
const fillPercent = maxSignups > 0 ? (currentSignups / maxSignups) * 100 : 0;
return (
<section
style={{
padding: "5rem 0",
background: "var(--color-background)",
position: "relative",
overflow: "hidden",
}}
>
{/* Ambient glow */}
<div
aria-hidden
style={{
position: "absolute",
top: "10%",
left: "50%",
transform: "translateX(-50%)",
width: 600,
height: 600,
borderRadius: "50%",
background: "var(--color-accent)",
opacity: 0.03,
filter: "blur(120px)",
pointerEvents: "none",
}}
/>
<div ref={ref} style={{ maxWidth: 640, margin: "0 auto", padding: "0 1.5rem", position: "relative", zIndex: 1 }}>
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6, ease: EASE }}
style={{ textAlign: "center", marginBottom: "2.5rem" }}
>
<span
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
padding: "0.375rem 0.875rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent)",
color: "var(--color-background)",
fontSize: "0.75rem",
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.05em",
marginBottom: "1.25rem",
}}
>
<Crown style={{ width: 12, height: 12 }} />
{badge}
</span>
<h2
style={{
fontSize: "clamp(1.5rem, 3.5vw, 2.5rem)",
fontWeight: 800,
color: "var(--color-foreground)",
letterSpacing: "-0.025em",
lineHeight: 1.15,
marginBottom: "0.75rem",
}}
>
{title}
</h2>
<p style={{ fontSize: "1rem", color: "var(--color-foreground-muted)", lineHeight: 1.65, maxWidth: 480, margin: "0 auto" }}>
{subtitle}
</p>
</motion.div>
{/* Progress counter */}
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.1, ease: EASE }}
style={{
padding: "1.25rem 1.5rem",
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-background-card)",
marginBottom: "1.5rem",
}}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<Users style={{ width: 14, height: 14, color: "var(--color-accent)" }} />
<span style={{ fontSize: "0.8125rem", fontWeight: 600, color: "var(--color-foreground)" }}>
{currentSignups.toLocaleString("fr-FR")} inscrits
</span>
</div>
<span style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)" }}>
{maxSignups.toLocaleString("fr-FR")} places
</span>
</div>
<div style={{ height: 6, borderRadius: 3, background: "var(--color-background-alt)", overflow: "hidden" }}>
<motion.div
initial={{ width: 0 }}
animate={inView ? { width: `${fillPercent}%` } : {}}
transition={{ duration: 1.2, delay: 0.3, ease: EASE }}
style={{ height: "100%", borderRadius: 3, background: "var(--color-accent)" }}
/>
</div>
<p style={{ fontSize: "0.75rem", color: "var(--color-foreground-light)", marginTop: 6, textAlign: "right" }}>
{Math.round(fillPercent)}% rempli
</p>
</motion.div>
{/* Form or confirmation */}
<AnimatePresence mode="wait">
{!submitted ? (
<motion.div
key="form"
initial={{ opacity: 0, y: 12 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.5, delay: 0.15, ease: EASE }}
style={{ marginBottom: "2rem" }}
>
<div style={{ display: "flex", gap: 8 }}>
<input
type="email"
placeholder={placeholder}
style={{
flex: 1,
padding: "0.875rem 1.25rem",
borderRadius: "var(--radius-md)",
border: "1px solid var(--color-border)",
background: "var(--color-background-card)",
color: "var(--color-foreground)",
fontSize: "0.9375rem",
outline: "none",
minWidth: 0,
}}
/>
<button
onClick={() => setSubmitted(true)}
style={{
padding: "0.875rem 1.5rem",
borderRadius: "var(--radius-md)",
background: "var(--color-accent)",
color: "var(--color-background)",
fontWeight: 600,
fontSize: "0.9375rem",
border: "none",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 6,
whiteSpace: "nowrap",
}}
>
{ctaLabel}
<ArrowRight style={{ width: 16, height: 16 }} />
</button>
</div>
{/* Social proof */}
{socialProof.length > 0 && (
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 8, marginTop: "1rem" }}>
<div style={{ display: "flex" }}>
{socialProof.map((person, i) => (
<div
key={person.initials}
style={{
width: 28,
height: 28,
borderRadius: "50%",
background: "var(--color-accent-subtle)",
border: "2px solid var(--color-background)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.5625rem",
fontWeight: 700,
color: "var(--color-accent)",
marginLeft: i > 0 ? -8 : 0,
}}
>
{person.initials}
</div>
))}
</div>
<span style={{ fontSize: "0.75rem", color: "var(--color-foreground-muted)" }}>
+{currentSignups.toLocaleString("fr-FR")} inscrits
</span>
</div>
)}
</motion.div>
) : (
<motion.div
key="confirmed"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, ease: EASE }}
style={{
padding: "2rem",
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-accent)",
background: "var(--color-accent-subtle)",
textAlign: "center",
marginBottom: "2rem",
}}
>
<Sparkles style={{ width: 28, height: 28, color: "var(--color-accent)", marginBottom: 12 }} />
<h3 style={{ fontSize: "1.125rem", fontWeight: 700, color: "var(--color-foreground)", marginBottom: "0.375rem" }}>
Vous etes inscrit !
</h3>
<p style={{ fontSize: "0.875rem", color: "var(--color-foreground-muted)" }}>
Position #{currentSignups + 1} — verifiez votre email pour confirmer.
</p>
</motion.div>
)}
</AnimatePresence>
{/* Tiers */}
{tiers.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: 0.25, ease: EASE }}
>
<p
style={{
fontSize: "0.75rem",
fontWeight: 600,
textTransform: "uppercase",
letterSpacing: "0.1em",
color: "var(--color-foreground-muted)",
textAlign: "center",
marginBottom: "1rem",
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: 6,
}}
>
<Star style={{ width: 12, height: 12, color: "var(--color-accent)" }} />
Paliers d'avantages
</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))", gap: 12 }}>
{tiers.map((tier, i) => (
<motion.div
key={tier.name}
initial={{ opacity: 0, y: 16 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.4, delay: 0.3 + i * 0.1, ease: EASE }}
style={{
padding: "1.25rem",
borderRadius: "var(--radius-lg)",
border: tier.active ? "2px solid var(--color-accent)" : "1px solid var(--color-border)",
background: tier.active ? "var(--color-accent-subtle)" : "var(--color-background-card)",
position: "relative",
}}
>
{tier.active && (
<span
style={{
position: "absolute",
top: -8,
left: "50%",
transform: "translateX(-50%)",
padding: "0.125rem 0.625rem",
borderRadius: "var(--radius-full)",
background: "var(--color-accent)",
color: "var(--color-background)",
fontSize: "0.625rem",
fontWeight: 700,
textTransform: "uppercase",
letterSpacing: "0.05em",
whiteSpace: "nowrap",
}}
>
Votre tier
</span>
)}
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: "0.375rem" }}>
<Zap style={{ width: 14, height: 14, color: tier.active ? "var(--color-accent)" : "var(--color-foreground-light)" }} />
<h4 style={{ fontSize: "0.875rem", fontWeight: 700, color: "var(--color-foreground)" }}>{tier.name}</h4>
</div>
<p style={{ fontSize: "0.6875rem", color: "var(--color-foreground-light)", marginBottom: "0.75rem" }}>
Inscrits {tier.range}
</p>
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
{tier.perks.map((perk) => (
<li
key={perk}
style={{
display: "flex",
alignItems: "center",
gap: 6,
fontSize: "0.75rem",
color: "var(--color-foreground-muted)",
marginBottom: 4,
}}
>
<Check style={{ width: 12, height: 12, color: "var(--color-accent)", flexShrink: 0 }} />
{perk}
</li>
))}
</ul>
</motion.div>
))}
</div>
</motion.div>
)}
</div>
</section>
);
}