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

Avis

Onboarding Carousel — React Onboarding Section — Incubator