# 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/)
