Retour au catalogue
Onboarding Carousel
Carousel d'onboarding plein ecran avec illustrations, navigation par points et bouton skip.
onboardingmedium Both Responsive a11y
playfulelegantsaasuniversalfullscreencarousel
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ArrowRight, ChevronRight } from "lucide-react";
interface Slide {
title: string;
description: string;
color: string;
}
interface OnboardingCarouselProps {
slides?: Slide[];
skipLabel?: string;
nextLabel?: string;
startLabel?: string;
}
const ease: [number, number, number, number] = [0.16, 1, 0.3, 1];
export default function OnboardingCarousel({
slides = [],
skipLabel = "Passer",
nextLabel = "Suivant",
startLabel = "Commencer",
}: OnboardingCarouselProps) {
const [current, setCurrent] = useState(0);
const [direction, setDirection] = useState(1);
const isLast = current === slides.length - 1;
const goTo = (index: number) => {
setDirection(index > current ? 1 : -1);
setCurrent(index);
};
const next = () => {
if (!isLast) goTo(current + 1);
};
const variants = {
enter: (d: number) => ({ x: d > 0 ? 300 : -300, opacity: 0 }),
center: { x: 0, opacity: 1 },
exit: (d: number) => ({ x: d > 0 ? -300 : 300, opacity: 0 }),
};
return (
<section
className="relative min-h-[600px] flex flex-col items-center justify-center overflow-hidden px-6 py-16"
style={{ background: "var(--color-background)" }}
>
{/* Skip button */}
{!isLast && (
<button
onClick={() => goTo(slides.length - 1)}
className="absolute top-6 right-6 text-sm font-medium z-10 flex items-center gap-1 transition-opacity hover:opacity-70"
style={{ color: "var(--color-foreground-muted)" }}
>
{skipLabel} <ChevronRight size={14} />
</button>
)}
{/* Slide content */}
<div className="relative w-full max-w-md h-[360px] flex items-center justify-center">
<AnimatePresence custom={direction} mode="wait">
<motion.div
key={current}
custom={direction}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 0.4, ease }}
className="absolute inset-0 flex flex-col items-center justify-center text-center"
>
{/* Illustration placeholder */}
<div
className="w-48 h-48 rounded-3xl mb-8 flex items-center justify-center"
style={{
background: `color-mix(in srgb, ${slides[current]?.color || "var(--color-accent)"} 12%, transparent)`,
border: `2px dashed color-mix(in srgb, ${slides[current]?.color || "var(--color-accent)"} 30%, transparent)`,
}}
>
<span
className="text-5xl font-bold"
style={{ color: slides[current]?.color || "var(--color-accent)", opacity: 0.3 }}
>
{current + 1}
</span>
</div>
<h2
className="text-2xl font-bold mb-3"
style={{ color: "var(--color-foreground)" }}
>
{slides[current]?.title}
</h2>
<p
className="text-sm leading-relaxed max-w-xs"
style={{ color: "var(--color-foreground-muted)" }}
>
{slides[current]?.description}
</p>
</motion.div>
</AnimatePresence>
</div>
{/* Dot navigation */}
<div className="flex items-center gap-2 mt-6">
{slides.map((_, i) => (
<button
key={i}
onClick={() => goTo(i)}
className="rounded-full transition-all duration-300"
style={{
width: i === current ? 24 : 8,
height: 8,
background:
i === current
? "var(--color-accent)"
: "var(--color-border)",
}}
/>
))}
</div>
{/* Action button */}
<motion.button
whileTap={{ scale: 0.97 }}
onClick={next}
className="mt-8 flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-semibold transition-opacity hover:opacity-90"
style={{
background: "var(--color-accent)",
color: "var(--color-background)",
}}
>
{isLast ? startLabel : nextLabel}
<ArrowRight size={16} />
</motion.button>
</section>
);
}