Docs
Product Tour

Product Tour

A premium step-by-step feature tour component for SaaS onboarding. Features glassmorphism design, buttery smooth animations, and elegant progress tracking.

Interactive Tour

Take a tour of our platform

Discover how our features work together to help you succeed

1Step 1 of 6

Welcome to Your SaaS Journey

Discover how our platform helps you build, scale, and succeed. We've designed every feature with your growth in mind, making it easy to go from idea to execution.

Intuitive interface designed for productivity
Get started in under 5 minutes
No credit card required for trial
Welcome to Your SaaS Journey
1/6
17%

Installation

Install dependencies

npm install framer-motion lucide-react

Copy and paste the following code into your project.

"use client";
 
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { ChevronLeft, ChevronRight, X, Check, Sparkles } from "lucide-react";
 
interface TourStep {
  id: string;
  title: string;
  description: string;
  image?: string;
  icon?: React.ElementType;
  features?: string[];
}
 
interface ProductTourProps {
  title?: string;
  description?: string;
  steps: TourStep[];
  className?: string;
  onComplete?: () => void;
  onSkip?: () => void;
}
 
export function ProductTour({
  title = "Take a tour of our platform",
  description = "Discover how our features work together to help you succeed",
  steps,
  className = "",
  onComplete,
  onSkip,
}: ProductTourProps) {
  const [currentStep, setCurrentStep] = useState(0);
  const [direction, setDirection] = useState(0);
 
  const goToStep = (index: number) => {
    setDirection(index > currentStep ? 1 : -1);
    setCurrentStep(index);
  };
 
  const nextStep = () => {
    if (currentStep < steps.length - 1) {
      setDirection(1);
      setCurrentStep(currentStep + 1);
    }
  };
 
  const prevStep = () => {
    if (currentStep > 0) {
      setDirection(-1);
      setCurrentStep(currentStep - 1);
    }
  };
 
  const slideVariants = {
    enter: (direction: number) => ({
      x: direction > 0 ? 300 : -300,
      opacity: 0,
    }),
    center: {
      x: 0,
      opacity: 1,
    },
    exit: (direction: number) => ({
      x: direction < 0 ? 300 : -300,
      opacity: 0,
    }),
  };
 
  const currentStepData = steps[currentStep];
  const StepIcon = currentStepData.icon;
  const progress = ((currentStep + 1) / steps.length) * 100;
 
  return (
    <section className={`relative w-full overflow-hidden py-24 ${className}`}>
      <div className="absolute inset-0">
        <div className="absolute inset-0 bg-gradient-to-br from-slate-50 via-blue-50/30 to-indigo-50/50 dark:from-zinc-950 dark:via-blue-950/10 dark:to-indigo-950/20" />
        <div className="absolute left-0 top-0 h-[500px] w-[500px] rounded-full bg-blue-400/20 blur-[120px] dark:bg-blue-600/10" />
        <div className="absolute bottom-0 right-0 h-[500px] w-[500px] rounded-full bg-indigo-400/20 blur-[120px] dark:bg-indigo-600/10" />
      </div>
 
      <div className="relative mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, ease: "easeOut" }}
          className="mb-16 text-center"
        >
          <div className="relative inline-block">
            <div className="mb-4 inline-flex items-center gap-2 rounded-full border border-blue-200/50 bg-blue-50/50 px-4 py-1.5 text-sm font-medium text-blue-700 backdrop-blur-sm dark:border-blue-800/50 dark:bg-blue-950/50 dark:text-blue-300">
              <Sparkles className="h-4 w-4" />
              Interactive Tour
            </div>
            <h2 className="mb-4 text-5xl font-bold tracking-tight text-zinc-900 dark:text-white lg:text-6xl">
              {title}
            </h2>
            {onSkip && (
              <button
                onClick={onSkip}
                className="absolute -right-16 top-0 rounded-full p-2 text-zinc-400 transition-all duration-200 hover:bg-zinc-100 hover:text-zinc-900 dark:hover:bg-zinc-800 dark:hover:text-white"
                aria-label="Skip tour"
              >
                <X className="h-5 w-5" />
              </button>
            )}
          </div>
          <p className="mx-auto mt-4 max-w-2xl text-lg text-zinc-600 dark:text-zinc-400">
            {description}
          </p>
        </motion.div>
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
          className="mb-12 flex items-center justify-center gap-3"
        >
          {steps.map((step, index) => (
            <button
              key={step.id}
              onClick={() => goToStep(index)}
              className="group relative"
            >
              <motion.div
                className={`relative h-2.5 overflow-hidden rounded-full transition-all duration-500 ${
                  index === currentStep
                    ? "w-16 bg-gradient-to-r from-blue-600 via-indigo-600 to-violet-600"
                    : index < currentStep
                      ? "w-2.5 bg-blue-500"
                      : "w-2.5 bg-zinc-300 dark:bg-zinc-700"
                }`}
              >
                {index === currentStep && (
                  <motion.div
                    className="absolute inset-0 bg-gradient-to-r from-transparent via-white/40 to-transparent"
                    animate={{ x: ["-100%", "200%"] }}
                    transition={{
                      duration: 2,
                      repeat: Infinity,
                      ease: "linear",
                    }}
                  />
                )}
              </motion.div>
              <div className="pointer-events-none absolute -top-12 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-lg bg-zinc-900 px-3 py-2 text-xs font-medium text-white opacity-0 shadow-xl transition-opacity duration-200 group-hover:opacity-100 dark:bg-white dark:text-zinc-900">
                {step.title}
                <div className="absolute -bottom-1 left-1/2 h-2 w-2 -translate-x-1/2 rotate-45 bg-zinc-900 dark:bg-white" />
              </div>
            </button>
          ))}
        </motion.div>
 
        <motion.div
          initial={{ opacity: 0, y: 30 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.7, delay: 0.2, ease: "easeOut" }}
          className="relative"
        >
          <div className="relative overflow-hidden rounded-3xl border border-white/60 bg-white/80 shadow-2xl shadow-zinc-900/10 backdrop-blur-2xl dark:border-zinc-800/60 dark:bg-zinc-900/80 dark:shadow-black/20">
            <div className="absolute inset-0 bg-gradient-to-br from-blue-500/[0.03] via-transparent to-indigo-500/[0.03]" />
 
            <AnimatePresence initial={false} custom={direction} mode="wait">
              <motion.div
                key={currentStep}
                custom={direction}
                variants={slideVariants}
                initial="enter"
                animate="center"
                exit="exit"
                transition={{
                  duration: 0.5,
                  ease: [0.32, 0.72, 0, 1],
                }}
              >
                <div className="relative grid gap-12 p-10 lg:grid-cols-2 lg:gap-16 lg:p-16">
                  <div className="flex flex-col justify-center space-y-8">
                    {StepIcon && (
                      <motion.div
                        initial={{ opacity: 0, scale: 0.8 }}
                        animate={{ opacity: 1, scale: 1 }}
                        transition={{ duration: 0.5, ease: "easeOut" }}
                        className="relative inline-flex h-16 w-16"
                      >
                        <div className="absolute inset-0 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-600 opacity-10 blur-2xl" />
                        <div className="relative flex h-full w-full items-center justify-center rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-600 shadow-lg shadow-blue-500/30">
                          {React.createElement(StepIcon, {
                            className: "h-8 w-8 text-white",
                          })}
                        </div>
                      </motion.div>
                    )}
                    <motion.div
                      initial={{ opacity: 0, x: -10 }}
                      animate={{ opacity: 1, x: 0 }}
                      transition={{
                        duration: 0.4,
                        delay: 0.1,
                        ease: "easeOut",
                      }}
                      className="inline-flex w-fit items-center gap-2 rounded-full bg-zinc-100 px-4 py-2 text-sm font-semibold text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"
                    >
                      <span className="flex h-5 w-5 items-center justify-center rounded-full bg-gradient-to-br from-blue-600 to-indigo-600 text-xs font-bold text-white">
                        {currentStep + 1}
                      </span>
                      Step {currentStep + 1} of {steps.length}
                    </motion.div>
 
                    <motion.h3
                      initial={{ opacity: 0, y: 10 }}
                      animate={{ opacity: 1, y: 0 }}
                      transition={{
                        duration: 0.5,
                        delay: 0.15,
                        ease: "easeOut",
                      }}
                      className="text-4xl font-bold leading-tight tracking-tight text-zinc-900 dark:text-white lg:text-5xl"
                    >
                      {currentStepData.title}
                    </motion.h3>
                    <motion.p
                      initial={{ opacity: 0, y: 10 }}
                      animate={{ opacity: 1, y: 0 }}
                      transition={{
                        duration: 0.5,
                        delay: 0.2,
                        ease: "easeOut",
                      }}
                      className="text-lg leading-relaxed text-zinc-600 dark:text-zinc-400"
                    >
                      {currentStepData.description}
                    </motion.p>
                    {currentStepData.features && (
                      <motion.div
                        initial={{ opacity: 0 }}
                        animate={{ opacity: 1 }}
                        transition={{
                          duration: 0.5,
                          delay: 0.25,
                          ease: "easeOut",
                        }}
                        className="space-y-3"
                      >
                        {currentStepData.features.map((feature, idx) => (
                          <motion.div
                            key={idx}
                            initial={{ opacity: 0, x: -10 }}
                            animate={{ opacity: 1, x: 0 }}
                            transition={{
                              duration: 0.4,
                              delay: 0.3 + idx * 0.08,
                              ease: "easeOut",
                            }}
                            className="flex items-center gap-3"
                          >
                            <div className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-emerald-500 to-green-600 shadow-sm">
                              <Check
                                className="h-3 w-3 text-white"
                                strokeWidth={3}
                              />
                            </div>
                            <span className="text-base text-zinc-700 dark:text-zinc-300">
                              {feature}
                            </span>
                          </motion.div>
                        ))}
                      </motion.div>
                    )}
 
                    <motion.div
                      initial={{ opacity: 0, y: 10 }}
                      animate={{ opacity: 1, y: 0 }}
                      transition={{
                        duration: 0.5,
                        delay: 0.35,
                        ease: "easeOut",
                      }}
                      className="flex items-center gap-3 pt-4"
                    >
                      <button
                        onClick={prevStep}
                        disabled={currentStep === 0}
                        className="group flex items-center gap-2 rounded-xl border border-zinc-300 bg-white px-5 py-3 font-semibold text-zinc-900 shadow-sm transition-all duration-200 hover:border-zinc-400 hover:shadow-md disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white dark:hover:border-zinc-600"
                      >
                        <ChevronLeft className="h-5 w-5 transition-transform duration-200 group-hover:-translate-x-0.5" />
                        Previous
                      </button>
 
                      {currentStep < steps.length - 1 ? (
                        <button
                          onClick={nextStep}
                          className="group relative overflow-hidden rounded-xl bg-gradient-to-r from-blue-600 to-indigo-600 px-7 py-3 font-semibold text-white shadow-lg shadow-blue-500/30 transition-all duration-200 hover:shadow-xl hover:shadow-blue-500/40"
                        >
                          <span className="relative z-10 flex items-center gap-2">
                            Next Step
                            <ChevronRight className="h-5 w-5 transition-transform duration-200 group-hover:translate-x-0.5" />
                          </span>
                        </button>
                      ) : (
                        <button
                          onClick={onComplete}
                          className="group relative overflow-hidden rounded-xl bg-gradient-to-r from-emerald-600 to-green-600 px-7 py-3 font-semibold text-white shadow-lg shadow-emerald-500/30 transition-all duration-200 hover:shadow-xl hover:shadow-emerald-500/40"
                        >
                          <span className="relative z-10 flex items-center gap-2">
                            <Check className="h-5 w-5" />
                            Get Started
                          </span>
                        </button>
                      )}
                    </motion.div>
                  </div>
 
                  {/* Right Image */}
                  {currentStepData.image && (
                    <motion.div
                      initial={{ opacity: 0, scale: 0.95 }}
                      animate={{ opacity: 1, scale: 1 }}
                      transition={{ duration: 0.6, ease: "easeOut" }}
                      className="relative flex items-center justify-center"
                    >
                      {/* Ambient glow */}
                      <div className="absolute inset-0 -m-4 rounded-3xl bg-gradient-to-br from-blue-500/10 via-indigo-500/10 to-violet-500/10 blur-3xl" />
 
                      {/* Image container */}
                      <div className="relative overflow-hidden rounded-2xl border border-zinc-200/80 bg-zinc-50 shadow-2xl dark:border-zinc-800/80 dark:bg-zinc-900">
                        <img
                          src={currentStepData.image}
                          alt={currentStepData.title}
                          className="h-full w-full object-cover"
                        />
                        {/* Subtle overlay */}
                        <div className="absolute inset-0 bg-gradient-to-t from-zinc-900/5 via-transparent to-transparent" />
                      </div>
                    </motion.div>
                  )}
                </div>
              </motion.div>
            </AnimatePresence>
            <div className="relative h-1 bg-zinc-100 dark:bg-zinc-800">
              <motion.div
                className="absolute inset-y-0 left-0 bg-gradient-to-r from-blue-600 via-indigo-600 to-violet-600"
                initial={{ width: 0 }}
                animate={{ width: `${progress}%` }}
                transition={{ duration: 0.5, ease: "easeOut" }}
              />
            </div>
          </div>
        </motion.div>
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, delay: 0.3, ease: "easeOut" }}
          className="mt-10 flex justify-center"
        >
          <div className="inline-flex items-center gap-4 rounded-full border border-zinc-200/80 bg-white/80 px-6 py-3 shadow-lg backdrop-blur-xl dark:border-zinc-800/80 dark:bg-zinc-900/80">
            <div className="flex items-baseline gap-1.5">
              <span className="text-3xl font-bold text-zinc-900 dark:text-white">
                {currentStep + 1}
              </span>
              <span className="text-lg text-zinc-400">/</span>
              <span className="text-lg font-medium text-zinc-600 dark:text-zinc-400">
                {steps.length}
              </span>
            </div>
            <div className="h-6 w-px bg-zinc-300 dark:bg-zinc-700" />
            <div className="flex items-center gap-2">
              <div className="h-2 w-24 overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-800">
                <motion.div
                  className="h-full rounded-full bg-gradient-to-r from-blue-600 to-indigo-600"
                  initial={{ width: 0 }}
                  animate={{ width: `${progress}%` }}
                  transition={{ duration: 0.5, ease: "easeOut" }}
                />
              </div>
              <span className="text-sm font-semibold text-zinc-600 dark:text-zinc-400">
                {Math.round(progress)}%
              </span>
            </div>
          </div>
        </motion.div>
      </div>
    </section>
  );
}

Features

  • Glassmorphism design - Premium frosted glass effect with backdrop blur
  • Buttery smooth animations - Custom easing curves for natural motion
  • Elegant progress indicators - Animated dots with shimmer effects
  • Feature highlights - Gradient checkmarks with smooth reveals
  • Image support - Display screenshots with ambient glows
  • Custom icons - Gradient icon containers with Lucide React
  • Callbacks - onComplete and onSkip handlers
  • Fully responsive - Optimized for all screen sizes
  • Dark mode ready - Beautiful in light and dark themes
  • TypeScript support - Full type safety
  • Ambient backgrounds - Soft gradient glows and overlays

Usage

Basic Usage

import ProductTour from "@/components/ui/product-tour";
import {Sparkles, Zap, Target} from "lucide-react";
 
const steps = [
  {
    id: "welcome",
    title: "Welcome to Our Platform",
    description: "Get started with our powerful features in just a few steps.",
    icon: Sparkles,
  },
  {
    id: "performance",
    title: "Lightning Fast Performance",
    description:
      "Experience blazing fast speeds with our optimized infrastructure.",
    icon: Zap,
  },
  {
    id: "goals",
    title: "Achieve Your Goals",
    description: "Track your progress and reach your objectives with ease.",
    icon: Target,
  },
];
 
export default function Page() {
  return <ProductTour steps={steps} />;
}

With Images and Features

const steps = [
  {
    id: "analytics",
    title: "Powerful Analytics Dashboard",
    description: "Get real-time insights into your business metrics.",
    icon: BarChart3,
    image:
      "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800&h=600&fit=crop",
    features: [
      "Real-time data visualization",
      "Custom reports and exports",
      "Predictive analytics powered by AI",
    ],
  },
  // ... more steps
];

With Callbacks

<ProductTour
  steps={steps}
  onComplete={() => {
    console.log("Tour completed!");
    router.push("/dashboard");
  }}
  onSkip={() => {
    console.log("Tour skipped");
    router.push("/dashboard");
  }}
/>

Custom Title and Description

<ProductTour
  title="Welcome to Your Journey"
  description="Let's explore the features that will transform your workflow"
  steps={steps}
/>

Props

ProductTourProps

PropTypeDefaultDescription
stepsTourStep[]RequiredArray of tour steps
titlestring"Take a tour of our platform"Section title
descriptionstring"Discover how our features work..."Section description
onComplete() => voidundefinedCallback when tour completes
onSkip() => voidundefinedCallback when tour is skipped
classNamestring""Additional CSS classes

TourStep

PropTypeRequiredDescription
idstringYesUnique identifier
titlestringYesStep title
descriptionstringYesStep description
iconReact.ElementTypeNoLucide React icon component
imagestringNoImage URL for visual reference
featuresstring[]NoList of features to highlight

TypeScript Interface

interface TourStep {
  id: string;
  title: string;
  description: string;
  image?: string;
  icon?: React.ElementType;
  features?: string[];
}
 
interface ProductTourProps {
  title?: string;
  description?: string;
  steps: TourStep[];
  className?: string;
  onComplete?: () => void;
  onSkip?: () => void;
}

Use Cases

Perfect for:

  • SaaS product onboarding
  • Feature announcements
  • New user tutorials
  • Product updates showcase
  • Interactive demos
  • User education flows
  • Beta feature introductions
  • Platform walkthroughs

Keyboard Navigation

Users can navigate through the tour using:

  • Next button - Proceed to next step
  • Previous button - Go back to previous step
  • Step indicators - Click any step to jump directly
  • Skip tour - Exit the tour at any time

Programmatic Navigation

// The component manages its own state internally
// Use callbacks to handle completion/skip events
 
<ProductTour
  steps={steps}
  onComplete={() => {
    // Save tour completion status
    localStorage.setItem("tourCompleted", "true");
    // Redirect to dashboard
    router.push("/dashboard");
  }}
  onSkip={() => {
    // Track skip event
    analytics.track("tour_skipped");
  }}
/>

Customization

Change Animation Speed

// Modify the transition duration in slideVariants
const slideVariants = {
  enter: (direction: number) => ({
    x: direction > 0 ? 300 : -300,
    opacity: 0,
  }),
  center: {
    x: 0,
    opacity: 1,
  },
  exit: (direction: number) => ({
    x: direction < 0 ? 300 : -300,
    opacity: 0,
  }),
};
 
// Then in the motion.div
transition={{
  duration: 0.7, // Increase for slower, decrease for faster
  ease: [0.32, 0.72, 0, 1],
}}

Change Color Scheme

// Replace gradient colors throughout the component
// Example: Change to purple/pink theme
 
// Progress indicators
className = "bg-gradient-to-r from-purple-600 via-pink-600 to-rose-600";
 
// Icon container
className = "bg-gradient-to-br from-purple-600 to-pink-600";
 
// Buttons
className = "bg-gradient-to-r from-purple-600 to-pink-600";
 
// Progress bar
className = "bg-gradient-to-r from-purple-600 via-pink-600 to-rose-600";

Adjust Glass Effect

// Modify the main card backdrop blur
className = "backdrop-blur-2xl"; // Change to backdrop-blur-xl or backdrop-blur-3xl
 
// Adjust opacity
className = "bg-white/80"; // Change to bg-white/60 for more transparency

Change Progress Indicator Style

// Replace the dots with circles or bars
<div className="flex items-center gap-2">
  {steps.map((_, index) => (
    <div
      className={`h-3 w-3 rounded-full transition-all ${
        index === currentStep
          ? "scale-125 bg-blue-600"
          : index < currentStep
            ? "bg-blue-400"
            : "bg-zinc-300"
      }`}
    />
  ))}
</div>

Add Video Support

interface TourStep {
  // ... existing props
  video?: string; // Add video URL
}
 
// In the component
{
  currentStepData.video && (
    <video
      src={currentStepData.video}
      autoPlay
      loop
      muted
      className="w-full rounded-2xl"
    />
  );
}

Add Confetti on Completion

import confetti from "canvas-confetti";
 
<ProductTour
  steps={steps}
  onComplete={() => {
    confetti({
      particleCount: 100,
      spread: 70,
      origin: {y: 0.6},
    });
    router.push("/dashboard");
  }}
/>;

Remove Background Effects

// For a cleaner look, remove the ambient background
// Delete or comment out the background divs:
 
{/* Remove these lines */}
<div className="absolute left-0 top-0 h-[500px] w-[500px] rounded-full bg-blue-400/20 blur-[120px]" />
<div className="absolute bottom-0 right-0 h-[500px] w-[500px] rounded-full bg-indigo-400/20 blur-[120px]" />

Add Custom Badge

// Add a custom badge above the title
<div className="mb-4 inline-flex items-center gap-2 rounded-full border border-blue-200/50 bg-blue-50/50 px-4 py-1.5">
  <Sparkles className="h-4 w-4" />
  <span className="text-sm font-medium">New Feature</span>
</div>

Common Patterns

import {Dialog, DialogContent} from "@/components/ui/dialog";
 
<Dialog open={showTour} onOpenChange={setShowTour}>
  <DialogContent className="max-w-5xl">
    <ProductTour
      steps={steps}
      onComplete={() => setShowTour(false)}
      onSkip={() => setShowTour(false)}
    />
  </DialogContent>
</Dialog>;

First-Time User Experience

"use client";
 
import {useEffect, useState} from "react";
 
export default function Page() {
  const [showTour, setShowTour] = useState(false);
 
  useEffect(() => {
    const hasSeenTour = localStorage.getItem("hasSeenTour");
    if (!hasSeenTour) {
      setShowTour(true);
    }
  }, []);
 
  const handleComplete = () => {
    localStorage.setItem("hasSeenTour", "true");
    setShowTour(false);
  };
 
  return (
    <>
      {showTour && (
        <ProductTour
          steps={steps}
          onComplete={handleComplete}
          onSkip={handleComplete}
        />
      )}
      {/* Your main content */}
    </>
  );
}

Feature Announcement

const newFeatureSteps = [
  {
    id: "new-feature",
    title: "🎉 New Feature: AI Assistant",
    description: "We've added an AI-powered assistant to help you work faster.",
    icon: Sparkles,
    image: "/features/ai-assistant.png",
    features: [
      "Natural language commands",
      "Smart suggestions",
      "24/7 availability",
    ],
  },
];
 
<ProductTour
  title="What's New"
  description="Check out our latest feature"
  steps={newFeatureSteps}
/>;

Multi-Step Onboarding

const onboardingSteps = [
  {
    id: "profile",
    title: "Complete Your Profile",
    description: "Add your details to personalize your experience.",
    icon: User,
  },
  {
    id: "team",
    title: "Invite Your Team",
    description: "Collaborate with your colleagues.",
    icon: Users,
  },
  {
    id: "integration",
    title: "Connect Your Tools",
    description: "Integrate with your favorite apps.",
    icon: Plug,
  },
];

Progress Tracking

"use client";
 
import {useState} from "react";
 
export default function OnboardingTour() {
  const [currentStep, setCurrentStep] = useState(0);
  const progress = ((currentStep + 1) / steps.length) * 100;
 
  return (
    <div>
      <div className="mb-4">
        <div className="text-sm text-zinc-600">
          Progress: {Math.round(progress)}%
        </div>
        <div className="h-2 w-full rounded-full bg-zinc-200">
          <div
            className="h-full rounded-full bg-blue-600 transition-all"
            style={{width: `${progress}%`}}
          />
        </div>
      </div>
      <ProductTour steps={steps} />
    </div>
  );
}

Design Details

Glassmorphism Effect

The component uses a sophisticated glassmorphism design with:

  • Backdrop blur - Creates the frosted glass effect
  • Semi-transparent backgrounds - White/80% opacity for depth
  • Layered gradients - Subtle color overlays for richness
  • Border transparency - Soft borders that blend naturally

Animation System

All animations use custom easing curves for natural motion:

  • Easing curve: [0.32, 0.72, 0, 1] - Custom bezier for smooth deceleration
  • Duration: 0.4-0.7s depending on element
  • Staggered delays: Sequential reveals for polish
  • Reduced motion: Respects user preferences

Color System

The component uses a refined gradient palette:

  • Primary: Blue 600 → Indigo 600 → Violet 600
  • Success: Emerald 600 → Green 600
  • Neutral: Zinc scale for text and borders
  • Ambient: Soft blue/indigo glows at 10-20% opacity

Best Practices

Content Guidelines

  1. Keep it concise - 2-3 sentences per step maximum
  2. Focus on benefits - Explain "why" not just "what"
  3. Use action words - "Discover", "Explore", "Create"
  4. Highlight value - Show how features solve problems
  5. Add visuals - Include screenshots or mockups
  6. Limit features - 3-4 feature bullets per step max

UX Guidelines

  1. Limit steps - 4-6 steps is ideal, max 8
  2. Allow skipping - Always provide a skip option
  3. Save progress - Remember if user has seen the tour
  4. Make it optional - Don't force users through it
  5. Time it right - Show after signup, not during
  6. Test on mobile - Ensure touch targets are large enough

Accessibility

// The component includes:
// - Semantic HTML structure
// - ARIA labels on buttons
// - Keyboard navigation support
// - Focus management
// - Screen reader friendly

Image Recommendations

Recommended Specs:

  • Format: PNG or WebP
  • Size: 800x600px (4:3 ratio)
  • Quality: High resolution for retina displays
  • Content: Actual product screenshots work best
  • Annotations: Add arrows or highlights to guide attention

Tools:

Analytics Integration

<ProductTour
  steps={steps}
  onComplete={() => {
    // Track completion
    analytics.track("product_tour_completed", {
      steps_viewed: steps.length,
      time_spent: Date.now() - startTime,
    });
  }}
  onSkip={() => {
    // Track skip
    analytics.track("product_tour_skipped", {
      step_number: currentStep + 1,
      step_id: steps[currentStep].id,
    });
  }}
/>

Examples

SaaS Onboarding

const saasSteps = [
  {
    id: "welcome",
    title: "Welcome to Acme SaaS",
    description: "Let's get you set up in under 2 minutes.",
    icon: Sparkles,
  },
  {
    id: "workspace",
    title: "Create Your Workspace",
    description: "Organize your projects and team in one place.",
    icon: Folder,
    image: "/tour/workspace.png",
  },
  {
    id: "invite",
    title: "Invite Your Team",
    description: "Collaboration is better together.",
    icon: Users,
    features: [
      "Unlimited team members",
      "Role-based permissions",
      "Real-time sync",
    ],
  },
];

E-commerce Tour

const ecommerceSteps = [
  {
    id: "products",
    title: "Browse Our Collection",
    description: "Discover thousands of products curated just for you.",
    icon: ShoppingBag,
  },
  {
    id: "wishlist",
    title: "Save Your Favorites",
    description: "Create wishlists and get notified of price drops.",
    icon: Heart,
  },
  {
    id: "checkout",
    title: "Fast & Secure Checkout",
    description: "Complete your purchase in seconds.",
    icon: CreditCard,
  },
];