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
1/6
17%
Installation
Install dependencies
npm install framer-motion lucide-reactCopy 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
| Prop | Type | Default | Description |
|---|---|---|---|
steps | TourStep[] | Required | Array of tour steps |
title | string | "Take a tour of our platform" | Section title |
description | string | "Discover how our features work..." | Section description |
onComplete | () => void | undefined | Callback when tour completes |
onSkip | () => void | undefined | Callback when tour is skipped |
className | string | "" | Additional CSS classes |
TourStep
| Prop | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier |
title | string | Yes | Step title |
description | string | Yes | Step description |
icon | React.ElementType | No | Lucide React icon component |
image | string | No | Image URL for visual reference |
features | string[] | No | List 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
Navigation
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 transparencyChange 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
Modal Version
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
- Keep it concise - 2-3 sentences per step maximum
- Focus on benefits - Explain "why" not just "what"
- Use action words - "Discover", "Explore", "Create"
- Highlight value - Show how features solve problems
- Add visuals - Include screenshots or mockups
- Limit features - 3-4 feature bullets per step max
UX Guidelines
- Limit steps - 4-6 steps is ideal, max 8
- Allow skipping - Always provide a skip option
- Save progress - Remember if user has seen the tour
- Make it optional - Don't force users through it
- Time it right - Show after signup, not during
- 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 friendlyImage 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:
- Screenshot: CleanShot X
- Mockups: Figma
- Annotations: Snagit
- Optimization: Squoosh
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,
},
];