2 min read
SaaSUser Onboarding Flows
User Onboarding für SaaS optimieren. Progressive Onboarding, Checklists, Product Tours und Activation Metrics.
User OnboardingActivationProduct TourChecklistTime-to-ValueRetention

User Onboarding Flows
Meta-Description: User Onboarding für SaaS optimieren. Progressive Onboarding, Checklists, Product Tours und Activation Metrics.
Keywords: User Onboarding, Activation, Product Tour, Checklist, Time-to-Value, Retention, SaaS Onboarding
Einführung
User Onboarding entscheidet über Trial-to-Paid Conversion und Retention. Der erste Eindruck zählt – 40-60% der Free Trial User kehren nach dem ersten Tag nie zurück. Dieser Guide zeigt effektive Onboarding-Strategien.
Onboarding Overview
┌─────────────────────────────────────────────────────────────┐
│ USER ONBOARDING JOURNEY │
├─────────────────────────────────────────────────────────────┤
│ │
│ Onboarding Phases: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. SIGNUP FLOW: │ │
│ │ ├── Minimal friction │ │
│ │ ├── Social login options │ │
│ │ └── Welcome message │ │
│ │ ↓ │ │
│ │ 2. INITIAL SETUP: │ │
│ │ ├── Profile completion │ │
│ │ ├── Preferences selection │ │
│ │ └── Team invitation (if B2B) │ │
│ │ ↓ │ │
│ │ 3. FIRST VALUE: │ │
│ │ ├── Guided first action │ │
│ │ ├── Quick win celebration │ │
│ │ └── "Aha moment" trigger │ │
│ │ ↓ │ │
│ │ 4. ACTIVATION: │ │
│ │ ├── Core features used │ │
│ │ ├── Habit formation │ │
│ │ └── Ready for conversion │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Key Metrics: │
│ ├── Time to First Value (TTFV) │
│ ├── Activation Rate │
│ ├── Onboarding Completion Rate │
│ ├── Day 1/7/30 Retention │
│ └── Trial-to-Paid Conversion │
│ │
│ Activation Events (Example): │
│ ├── Created first project │
│ ├── Invited team member │
│ ├── Connected integration │
│ └── Completed first workflow │
│ │
└─────────────────────────────────────────────────────────────┘Onboarding Checklist
// lib/onboarding/types.ts
export interface OnboardingStep {
id: string;
title: string;
description: string;
action: {
type: 'link' | 'modal' | 'trigger';
target: string;
};
completionEvent: string;
points?: number;
required?: boolean;
order: number;
}
export interface UserOnboarding {
userId: string;
completedSteps: string[];
currentStep: string | null;
startedAt: Date;
completedAt: Date | null;
skippedAt: Date | null;
}
// lib/onboarding/config.ts
export const onboardingSteps: OnboardingStep[] = [
{
id: 'complete-profile',
title: 'Complete your profile',
description: 'Add your name and profile picture',
action: { type: 'link', target: '/settings/profile' },
completionEvent: 'profile_completed',
points: 10,
required: true,
order: 1
},
{
id: 'create-project',
title: 'Create your first project',
description: 'Set up a project to organize your work',
action: { type: 'modal', target: 'create-project-modal' },
completionEvent: 'project_created',
points: 20,
required: true,
order: 2
},
{
id: 'invite-team',
title: 'Invite your team',
description: 'Collaborate with your colleagues',
action: { type: 'link', target: '/settings/team' },
completionEvent: 'team_member_invited',
points: 15,
required: false,
order: 3
},
{
id: 'connect-integration',
title: 'Connect an integration',
description: 'Sync with your favorite tools',
action: { type: 'link', target: '/settings/integrations' },
completionEvent: 'integration_connected',
points: 20,
required: false,
order: 4
},
{
id: 'complete-workflow',
title: 'Complete your first workflow',
description: 'Experience the full power of our platform',
action: { type: 'trigger', target: 'start-workflow-tour' },
completionEvent: 'workflow_completed',
points: 35,
required: true,
order: 5
}
];
export const ACTIVATION_THRESHOLD = 3; // Steps to be "activated"// lib/onboarding/service.ts
import { db } from '@/lib/db';
import { onboardingSteps, ACTIVATION_THRESHOLD } from './config';
import { track } from '@/lib/analytics';
export async function getOnboardingProgress(userId: string) {
const onboarding = await db.userOnboarding.findUnique({
where: { userId }
});
if (!onboarding) {
// Initialize onboarding
return db.userOnboarding.create({
data: {
userId,
completedSteps: [],
currentStep: onboardingSteps[0].id,
startedAt: new Date()
}
});
}
return onboarding;
}
export async function completeOnboardingStep(
userId: string,
stepId: string
): Promise<{ completed: boolean; isActivated: boolean; nextStep: string | null }> {
const onboarding = await getOnboardingProgress(userId);
if (onboarding.completedSteps.includes(stepId)) {
return {
completed: false,
isActivated: isUserActivated(onboarding.completedSteps),
nextStep: onboarding.currentStep
};
}
const step = onboardingSteps.find(s => s.id === stepId);
if (!step) {
throw new Error(`Unknown step: ${stepId}`);
}
const completedSteps = [...onboarding.completedSteps, stepId];
const nextStep = getNextStep(completedSteps);
const isCompleted = nextStep === null;
await db.userOnboarding.update({
where: { userId },
data: {
completedSteps,
currentStep: nextStep,
...(isCompleted && { completedAt: new Date() })
}
});
// Track analytics
track('onboarding_step_completed', {
userId,
stepId,
stepTitle: step.title,
points: step.points,
totalCompleted: completedSteps.length,
isActivated: isUserActivated(completedSteps)
});
// Award points (gamification)
if (step.points) {
await awardPoints(userId, step.points, `Completed: ${step.title}`);
}
// Check activation
const activated = isUserActivated(completedSteps);
if (activated && !isUserActivated(onboarding.completedSteps)) {
await handleUserActivation(userId);
}
return {
completed: true,
isActivated: activated,
nextStep
};
}
function getNextStep(completedSteps: string[]): string | null {
const remaining = onboardingSteps
.filter(s => !completedSteps.includes(s.id))
.sort((a, b) => a.order - b.order);
return remaining[0]?.id || null;
}
function isUserActivated(completedSteps: string[]): boolean {
const requiredSteps = onboardingSteps.filter(s => s.required);
const completedRequired = requiredSteps.filter(s =>
completedSteps.includes(s.id)
);
return completedRequired.length >= ACTIVATION_THRESHOLD;
}
async function handleUserActivation(userId: string) {
// Update user status
await db.user.update({
where: { id: userId },
data: { isActivated: true, activatedAt: new Date() }
});
// Track activation
track('user_activated', { userId });
// Send activation email
await sendActivationEmail(userId);
// Notify sales (for high-value leads)
await notifySalesIfQualified(userId);
}Onboarding UI Components
// components/onboarding/OnboardingChecklist.tsx
'use client';
import { useState, useEffect } from 'react';
import { CheckCircle, Circle, ChevronRight, X } from 'lucide-react';
import { onboardingSteps, OnboardingStep } from '@/lib/onboarding/config';
interface OnboardingProgress {
completedSteps: string[];
currentStep: string | null;
}
export function OnboardingChecklist() {
const [progress, setProgress] = useState<OnboardingProgress | null>(null);
const [isExpanded, setIsExpanded] = useState(true);
const [isDismissed, setIsDismissed] = useState(false);
useEffect(() => {
fetch('/api/onboarding/progress')
.then(res => res.json())
.then(setProgress);
}, []);
if (!progress || isDismissed) return null;
const completedCount = progress.completedSteps.length;
const totalCount = onboardingSteps.length;
const progressPercent = (completedCount / totalCount) * 100;
// Hide if completed
if (completedCount === totalCount) return null;
return (
<div className="fixed bottom-4 right-4 w-80 bg-white rounded-lg shadow-lg border">
{/* Header */}
<div
className="flex items-center justify-between p-4 cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-center gap-3">
<div className="relative w-10 h-10">
<svg className="w-10 h-10 -rotate-90">
<circle
cx="20" cy="20" r="16"
fill="none"
stroke="#e5e7eb"
strokeWidth="4"
/>
<circle
cx="20" cy="20" r="16"
fill="none"
stroke="#3b82f6"
strokeWidth="4"
strokeDasharray={`${progressPercent} 100`}
strokeLinecap="round"
/>
</svg>
<span className="absolute inset-0 flex items-center justify-center text-sm font-semibold">
{completedCount}/{totalCount}
</span>
</div>
<div>
<h3 className="font-semibold">Getting Started</h3>
<p className="text-sm text-gray-500">
{completedCount === 0 ? "Let's set you up!" : `${totalCount - completedCount} steps left`}
</p>
</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); setIsDismissed(true); }}
className="text-gray-400 hover:text-gray-600"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Steps */}
{isExpanded && (
<div className="border-t max-h-80 overflow-y-auto">
{onboardingSteps.map((step) => (
<OnboardingStepItem
key={step.id}
step={step}
isCompleted={progress.completedSteps.includes(step.id)}
isCurrent={progress.currentStep === step.id}
/>
))}
</div>
)}
</div>
);
}
function OnboardingStepItem({
step,
isCompleted,
isCurrent
}: {
step: OnboardingStep;
isCompleted: boolean;
isCurrent: boolean;
}) {
const handleClick = () => {
if (isCompleted) return;
if (step.action.type === 'link') {
window.location.href = step.action.target;
} else if (step.action.type === 'modal') {
// Dispatch event to open modal
window.dispatchEvent(new CustomEvent('open-modal', {
detail: { id: step.action.target }
}));
} else if (step.action.type === 'trigger') {
window.dispatchEvent(new CustomEvent(step.action.target));
}
};
return (
<button
onClick={handleClick}
disabled={isCompleted}
className={`w-full flex items-center gap-3 p-4 text-left hover:bg-gray-50 transition ${
isCompleted ? 'opacity-60' : ''
} ${isCurrent ? 'bg-blue-50' : ''}`}
>
{isCompleted ? (
<CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0" />
) : (
<Circle className={`w-5 h-5 flex-shrink-0 ${isCurrent ? 'text-blue-500' : 'text-gray-300'}`} />
)}
<div className="flex-1 min-w-0">
<p className={`font-medium ${isCompleted ? 'line-through' : ''}`}>
{step.title}
</p>
<p className="text-sm text-gray-500 truncate">
{step.description}
</p>
</div>
{!isCompleted && (
<ChevronRight className="w-5 h-5 text-gray-400 flex-shrink-0" />
)}
</button>
);
}Product Tour
// components/onboarding/ProductTour.tsx
'use client';
import { useState, useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
interface TourStep {
target: string; // CSS selector
title: string;
content: string;
placement: 'top' | 'bottom' | 'left' | 'right';
action?: {
label: string;
onClick: () => void;
};
}
interface ProductTourProps {
tourId: string;
steps: TourStep[];
onComplete: () => void;
onSkip?: () => void;
}
export function ProductTour({
tourId,
steps,
onComplete,
onSkip
}: ProductTourProps) {
const [currentStep, setCurrentStep] = useState(0);
const [targetRect, setTargetRect] = useState<DOMRect | null>(null);
const step = steps[currentStep];
// Find and highlight target element
useEffect(() => {
const target = document.querySelector(step.target);
if (target) {
setTargetRect(target.getBoundingClientRect());
// Scroll into view
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Add highlight class
target.classList.add('tour-highlight');
return () => {
target.classList.remove('tour-highlight');
};
}
}, [step.target]);
const handleNext = useCallback(() => {
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1);
} else {
onComplete();
}
}, [currentStep, steps.length, onComplete]);
const handlePrev = () => {
if (currentStep > 0) {
setCurrentStep(currentStep - 1);
}
};
const handleSkip = () => {
onSkip?.();
};
if (!targetRect) return null;
// Calculate tooltip position
const tooltipStyle = calculateTooltipPosition(targetRect, step.placement);
return createPortal(
<>
{/* Overlay with spotlight */}
<div className="fixed inset-0 z-50">
<svg className="w-full h-full">
<defs>
<mask id="spotlight-mask">
<rect width="100%" height="100%" fill="white" />
<rect
x={targetRect.x - 8}
y={targetRect.y - 8}
width={targetRect.width + 16}
height={targetRect.height + 16}
rx="8"
fill="black"
/>
</mask>
</defs>
<rect
width="100%"
height="100%"
fill="rgba(0,0,0,0.5)"
mask="url(#spotlight-mask)"
/>
</svg>
</div>
{/* Tooltip */}
<div
className="fixed z-50 w-80 bg-white rounded-lg shadow-xl p-4"
style={tooltipStyle}
>
{/* Progress */}
<div className="flex gap-1 mb-3">
{steps.map((_, i) => (
<div
key={i}
className={`h-1 flex-1 rounded ${
i <= currentStep ? 'bg-blue-500' : 'bg-gray-200'
}`}
/>
))}
</div>
<h3 className="font-semibold text-lg mb-2">{step.title}</h3>
<p className="text-gray-600 mb-4">{step.content}</p>
{/* Actions */}
<div className="flex items-center justify-between">
<button
onClick={handleSkip}
className="text-sm text-gray-500 hover:text-gray-700"
>
Skip tour
</button>
<div className="flex gap-2">
{currentStep > 0 && (
<button
onClick={handlePrev}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
>
Back
</button>
)}
<button
onClick={handleNext}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
{currentStep === steps.length - 1 ? 'Finish' : 'Next'}
</button>
</div>
</div>
</div>
</>,
document.body
);
}
function calculateTooltipPosition(
targetRect: DOMRect,
placement: string
): React.CSSProperties {
const offset = 16;
switch (placement) {
case 'bottom':
return {
top: targetRect.bottom + offset,
left: targetRect.left + targetRect.width / 2,
transform: 'translateX(-50%)'
};
case 'top':
return {
bottom: window.innerHeight - targetRect.top + offset,
left: targetRect.left + targetRect.width / 2,
transform: 'translateX(-50%)'
};
case 'right':
return {
top: targetRect.top + targetRect.height / 2,
left: targetRect.right + offset,
transform: 'translateY(-50%)'
};
case 'left':
return {
top: targetRect.top + targetRect.height / 2,
right: window.innerWidth - targetRect.left + offset,
transform: 'translateY(-50%)'
};
default:
return {};
}
}
// CSS for highlighting
/*
.tour-highlight {
position: relative;
z-index: 51;
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5);
border-radius: 8px;
}
*/Activation Tracking
// lib/onboarding/activation.ts
import { db } from '@/lib/db';
import { track } from '@/lib/analytics';
interface ActivationEvent {
userId: string;
event: string;
metadata?: Record<string, unknown>;
}
// Define activation criteria
const ACTIVATION_EVENTS = [
{ event: 'project_created', weight: 2 },
{ event: 'team_member_invited', weight: 1 },
{ event: 'integration_connected', weight: 2 },
{ event: 'workflow_completed', weight: 3 },
{ event: 'dashboard_customized', weight: 1 }
];
const ACTIVATION_THRESHOLD = 5; // Total weight needed
export async function trackActivationEvent(
event: ActivationEvent
): Promise<{ isActivated: boolean; progress: number }> {
const { userId, event: eventName, metadata } = event;
// Record the event
await db.userEvent.create({
data: {
userId,
eventName,
metadata: metadata || {},
occurredAt: new Date()
}
});
// Calculate activation progress
const userEvents = await db.userEvent.findMany({
where: { userId },
select: { eventName: true }
});
const uniqueEvents = new Set(userEvents.map(e => e.eventName));
let totalWeight = 0;
ACTIVATION_EVENTS.forEach(ae => {
if (uniqueEvents.has(ae.event)) {
totalWeight += ae.weight;
}
});
const progress = Math.min(100, (totalWeight / ACTIVATION_THRESHOLD) * 100);
const isActivated = totalWeight >= ACTIVATION_THRESHOLD;
// Check if user just became activated
const user = await db.user.findUnique({
where: { id: userId },
select: { isActivated: true }
});
if (isActivated && !user?.isActivated) {
await activateUser(userId);
}
return { isActivated, progress };
}
async function activateUser(userId: string) {
const activationTime = await calculateTimeToActivation(userId);
await db.user.update({
where: { id: userId },
data: {
isActivated: true,
activatedAt: new Date()
}
});
track('user_activated', {
userId,
timeToActivation: activationTime
});
// Trigger post-activation flow
await triggerPostActivation(userId);
}
async function calculateTimeToActivation(userId: string): Promise<number> {
const user = await db.user.findUnique({
where: { id: userId },
select: { createdAt: true }
});
if (!user) return 0;
return Date.now() - user.createdAt.getTime();
}
async function triggerPostActivation(userId: string) {
// Send congratulations email
await sendEmail(userId, 'activation-congrats');
// Schedule upgrade prompt
await scheduleEmail(userId, 'upgrade-prompt', { delay: '3d' });
// Notify sales for qualified leads
const user = await db.user.findUnique({
where: { id: userId },
include: { tenant: { include: { subscription: true } } }
});
if (user?.tenant && isQualifiedLead(user.tenant)) {
await notifySales(userId, 'activated_qualified_lead');
}
}Best Practices
| Aspect | Recommendation |
|---|---|
| **Time to Value** | < 5 minutes to first success |
| **Steps** | Max 5-7 onboarding steps |
| **Progress** | Always show completion % |
| **Personalization** | Tailor based on use case |
| **Help** | Offer chat/support at each step |
| **Celebration** | Acknowledge completions |
Fazit
Effektives User Onboarding:
- Progressive Disclosure: Nicht alles auf einmal zeigen
- Quick Wins: Erste Erfolge ermöglichen
- Tracking: Activation Events messen
- Iteration: Kontinuierlich verbessern
Gutes Onboarding ist der Schlüssel zu Retention.
Bildprompts
- "Onboarding checklist UI, progress bar and completed steps"
- "Product tour spotlight on feature, tooltip explanation"
- "User activation funnel, stages from signup to activated"