Menu
Nazad na Blog
2 min read
SaaS

User Onboarding Flows

User Onboarding für SaaS optimieren. Progressive Onboarding, Checklists, Product Tours und Activation Metrics.

User OnboardingActivationProduct TourChecklistTime-to-ValueRetention
User Onboarding Flows

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

AspectRecommendation
**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