Files
Portfolio/blog-posts/87-user-onboarding-flows.md
damjan_savicandClaude Opus 4.5 e1bbe5455d Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit:
- Blog-System mit 100+ Artikeln
- Supabase-Integration
- Responsive Design mit Tailwind CSS
- TypeScript-Konfiguration
- Testing-Setup mit Vitest und Playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 15:07:20 +01:00

789 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```typescript
// 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"
```
```typescript
// 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
```tsx
// 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
```tsx
// 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
```typescript
// 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:
1. **Progressive Disclosure**: Nicht alles auf einmal zeigen
2. **Quick Wins**: Erste Erfolge ermöglichen
3. **Tracking**: Activation Events messen
4. **Iteration**: Kontinuierlich verbessern
Gutes Onboarding ist der Schlüssel zu Retention.
---
## Bildprompts
1. "Onboarding checklist UI, progress bar and completed steps"
2. "Product tour spotlight on feature, tooltip explanation"
3. "User activation funnel, stages from signup to activated"
---
## Quellen
- [Appcues Onboarding Guide](https://www.appcues.com/blog/user-onboarding)
- [UserPilot Onboarding](https://userpilot.com/blog/user-onboarding/)
- [Product-Led Growth Onboarding](https://www.productled.org/foundations/user-onboarding)
- [Intercom Onboarding](https://www.intercom.com/blog/user-onboarding-best-practices/)