---
name: onboarding-ux
description: Design and implement user onboarding flows for B2B SaaS including signup wizards, progress tracking, activation checklists, and first-use experiences. Use when building signup flows, dashboard onboarding, or improving user activation for Mawidi's clinic customers.
---

# Onboarding UX Skill

## Overview

Design and implement user onboarding flows that convert signups into activated B2B customers. This skill covers signup wizards, progress tracking, activation checklists, and first-use experiences tailored for medical clinic operators.

**Onboarding Goals:**

- Reduce time-to-value (first appointment booked)
- Increase activation rate (complete setup)
- Improve user confidence with the platform
- Support Arabic/English bilingual experience

## When to Use

Use this skill when:

- Building signup flows
- Creating dashboard onboarding
- Implementing progress checklists
- Designing first-use experiences
- Improving activation metrics
- Adding contextual help

## Mawidi Branding in Onboarding

Always include Mawidi branding in onboarding flows:

### Logo Placement

```tsx
// Signup page header
<div className="flex flex-col items-center mb-8">
  <img
    src="/brand/logo-stacked.jpg"
    alt="Mawidi - موعدي"
    className="h-20 w-auto mb-4"
  />
  <h1 className="text-3xl font-bold text-[#1E293B]">
    {t.title}
  </h1>
</div>

// Minimal header variant
<div className="flex justify-center py-6">
  <img
    src="/brand/logo-horizontal.jpeg"
    alt="Mawidi"
    className="h-10 w-auto"
  />
</div>
```

### Brand Colors for Onboarding

```css
:root {
  /* Primary CTA - WhatsApp green */
  --mawidi-green: #25d366;
  --mawidi-green-hover: #128c7e;

  /* Text and backgrounds */
  --mawidi-ink: #1e293b;
  --mawidi-sand: #f5f5f5;

  /* Success states */
  --mawidi-success: #10b981;
}
```

### CTA Button Styling

```tsx
// Primary CTA for onboarding
<button className="w-full bg-[#25D366] hover:bg-[#128C7E] text-white py-3 px-6 rounded-lg font-medium transition-colors">
  Continue
</button>

// Secondary action
<button className="w-full border border-[#1E293B] text-[#1E293B] py-3 px-6 rounded-lg font-medium hover:bg-gray-50">
  Skip for now
</button>
```

## Signup Flow Architecture

### Multi-Step Signup Wizard

```
Step 1: Email/Phone Verification
        ↓
Step 2: Company Information
        ↓
Step 3: Plan Selection
        ↓
Step 4: Payment (if required)
        ↓
Step 5: Initial Setup Wizard
        ↓
Dashboard with Onboarding Checklist
```

### Signup Flow Pages

```
app/[lang]/signup/
├── page.tsx           # Step 1: Email entry + OTP
├── verify/
│   └── page.tsx       # Step 1b: OTP verification
├── company-info/
│   └── page.tsx       # Step 2: Company details
├── onboarding/
│   └── page.tsx       # Step 5: Initial setup
└── success/
    └── page.tsx       # Completion + next steps
```

## Step-by-Step Implementation

### Step 1: Email/Phone Verification

```tsx
// app/[lang]/signup/page.tsx

"use client";

import { useState } from "react";
import { useRouter, useParams } from "next/navigation";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const emailSchema = z.object({
  email: z.string().email("Please enter a valid email"),
});

export default function SignupPage() {
  const router = useRouter();
  const { lang } = useParams();
  const [isLoading, setIsLoading] = useState(false);

  const t =
    {
      en: {
        title: "Get Started with Mawidi",
        subtitle: "Enter your email to begin your free trial",
        emailLabel: "Work Email",
        emailPlaceholder: "you@clinic.com",
        continue: "Continue",
        haveAccount: "Already have an account?",
        login: "Log in",
      },
      ar: {
        title: "ابدأ مع مواعيدي",
        subtitle: "أدخل بريدك الإلكتروني لبدء الفترة التجريبية",
        emailLabel: "البريد الإلكتروني للعمل",
        emailPlaceholder: "you@clinic.com",
        continue: "متابعة",
        haveAccount: "لديك حساب بالفعل؟",
        login: "تسجيل الدخول",
      },
    }[lang as string] || {};

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(emailSchema),
  });

  const onSubmit = async (data: { email: string }) => {
    setIsLoading(true);
    try {
      await fetch("/api/send-otp", {
        method: "POST",
        body: JSON.stringify({ email: data.email }),
      });

      // Store email in session storage for next step
      sessionStorage.setItem("signup_email", data.email);
      router.push(`/${lang}/signup/verify`);
    } catch (error) {
      // Handle error
    } finally {
      setIsLoading(false);
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center p-4">
      <div className="max-w-md w-full">
        {/* Progress indicator */}
        <div className="flex justify-center mb-8">
          <StepIndicator current={1} total={4} />
        </div>

        <h1 className="text-3xl font-bold text-center mb-2">{t.title}</h1>
        <p className="text-gray-600 text-center mb-8">{t.subtitle}</p>

        <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
          <div>
            <label className="block text-sm font-medium mb-1">
              {t.emailLabel}
            </label>
            <input
              type="email"
              {...register("email")}
              placeholder={t.emailPlaceholder}
              className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-mawidi-green"
            />
            {errors.email && (
              <p className="text-red-500 text-sm mt-1">
                {errors.email.message}
              </p>
            )}
          </div>

          <button
            type="submit"
            disabled={isLoading}
            className="w-full bg-mawidi-green text-white py-3 rounded-lg font-medium hover:bg-mawidi-green-hover disabled:opacity-50"
          >
            {isLoading ? "..." : t.continue}
          </button>
        </form>

        <p className="text-center mt-6 text-gray-600">
          {t.haveAccount}{" "}
          <a
            href={`/${lang}/login`}
            className="text-mawidi-green hover:underline"
          >
            {t.login}
          </a>
        </p>
      </div>
    </div>
  );
}
```

### Step Progress Indicator

```tsx
// components/onboarding/StepIndicator.tsx

interface StepIndicatorProps {
  current: number;
  total: number;
  labels?: string[];
}

export function StepIndicator({ current, total, labels }: StepIndicatorProps) {
  return (
    <div className="flex items-center gap-2">
      {Array.from({ length: total }, (_, i) => (
        <div key={i} className="flex items-center">
          {/* Step circle */}
          <div
            className={`
              w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium
              ${
                i + 1 < current
                  ? "bg-mawidi-green text-white"
                  : i + 1 === current
                    ? "bg-mawidi-green text-white ring-4 ring-mawidi-green/20"
                    : "bg-gray-200 text-gray-500"
              }
            `}
          >
            {i + 1 < current ? <CheckIcon className="w-5 h-5" /> : i + 1}
          </div>

          {/* Connector line */}
          {i < total - 1 && (
            <div
              className={`
                w-12 h-0.5 mx-1
                ${i + 1 < current ? "bg-mawidi-green" : "bg-gray-200"}
              `}
            />
          )}
        </div>
      ))}
    </div>
  );
}
```

### Step 2: Company Information

```tsx
// app/[lang]/signup/company-info/page.tsx

const companyInfoSchema = z.object({
  companyName: z.string().min(2, "Company name is required"),
  industry: z.enum(["healthcare", "dental", "beauty", "other"]),
  companySize: z.enum(["1-5", "6-20", "21-50", "51+"]),
  phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone number"),
});

export default function CompanyInfoPage() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(companyInfoSchema),
  });

  const onSubmit = async (data: CompanyInfo) => {
    // Save company info
    await fetch("/api/signup/company-info", {
      method: "POST",
      body: JSON.stringify(data),
    });

    router.push(`/${lang}/pricing?signup=true`);
  };

  return (
    <div className="min-h-screen flex items-center justify-center p-4">
      <div className="max-w-md w-full">
        <StepIndicator current={2} total={4} />

        <h1 className="text-3xl font-bold text-center mb-2">
          Tell us about your business
        </h1>

        <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
          {/* Company Name */}
          <div>
            <label className="block text-sm font-medium mb-1">
              Company Name
            </label>
            <input
              {...register("companyName")}
              className="w-full px-4 py-3 border rounded-lg"
              placeholder="Acme Dental Clinic"
            />
          </div>

          {/* Industry */}
          <div>
            <label className="block text-sm font-medium mb-1">Industry</label>
            <select
              {...register("industry")}
              className="w-full px-4 py-3 border rounded-lg"
            >
              <option value="">Select industry...</option>
              <option value="healthcare">Healthcare / Medical</option>
              <option value="dental">Dental</option>
              <option value="beauty">Beauty & Wellness</option>
              <option value="other">Other</option>
            </select>
          </div>

          {/* Company Size */}
          <div>
            <label className="block text-sm font-medium mb-1">Team Size</label>
            <div className="grid grid-cols-4 gap-2">
              {["1-5", "6-20", "21-50", "51+"].map((size) => (
                <label
                  key={size}
                  className="flex items-center justify-center p-3 border rounded-lg cursor-pointer hover:border-mawidi-green"
                >
                  <input
                    type="radio"
                    value={size}
                    {...register("companySize")}
                    className="sr-only"
                  />
                  <span>{size}</span>
                </label>
              ))}
            </div>
          </div>

          <button
            type="submit"
            className="w-full bg-mawidi-green text-white py-3 rounded-lg"
          >
            Continue
          </button>
        </form>
      </div>
    </div>
  );
}
```

## Dashboard Onboarding Checklist

### Checklist Component

```tsx
// components/dashboard/OnboardingChecklist.tsx

interface ChecklistItem {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  action: () => void;
  actionLabel: string;
}

export function OnboardingChecklist({
  items,
  lang,
}: {
  items: ChecklistItem[];
  lang: "ar" | "en";
}) {
  const completedCount = items.filter((i) => i.completed).length;
  const progress = (completedCount / items.length) * 100;

  const t = {
    en: {
      title: "Get Started",
      subtitle: `${completedCount} of ${items.length} completed`,
      dismiss: "Dismiss",
    },
    ar: {
      title: "ابدأ الآن",
      subtitle: `${completedCount} من ${items.length} مكتمل`,
      dismiss: "إغلاق",
    },
  }[lang];

  return (
    <div className="bg-white rounded-xl border p-6 mb-6">
      <div className="flex justify-between items-start mb-4">
        <div>
          <h2 className="text-xl font-semibold">{t.title}</h2>
          <p className="text-gray-500 text-sm">{t.subtitle}</p>
        </div>
        <button className="text-gray-400 hover:text-gray-600 text-sm">
          {t.dismiss}
        </button>
      </div>

      {/* Progress bar */}
      <div className="h-2 bg-gray-100 rounded-full mb-6 overflow-hidden">
        <div
          className="h-full bg-mawidi-green rounded-full transition-all duration-500"
          style={{ width: `${progress}%` }}
        />
      </div>

      {/* Checklist items */}
      <div className="space-y-4">
        {items.map((item, index) => (
          <div
            key={item.id}
            className={`
              flex items-center gap-4 p-4 rounded-lg border
              ${item.completed ? "bg-gray-50 border-gray-200" : "border-mawidi-green/20 bg-mawidi-green/5"}
            `}
          >
            {/* Checkbox */}
            <div
              className={`
                w-6 h-6 rounded-full flex items-center justify-center flex-shrink-0
                ${
                  item.completed
                    ? "bg-mawidi-green text-white"
                    : "border-2 border-gray-300"
                }
              `}
            >
              {item.completed && <CheckIcon className="w-4 h-4" />}
            </div>

            {/* Content */}
            <div className="flex-1">
              <h3
                className={`font-medium ${item.completed ? "line-through text-gray-500" : ""}`}
              >
                {item.title}
              </h3>
              <p className="text-sm text-gray-500">{item.description}</p>
            </div>

            {/* Action */}
            {!item.completed && (
              <button
                onClick={item.action}
                className="px-4 py-2 bg-mawidi-green text-white rounded-lg text-sm hover:bg-mawidi-green-hover"
              >
                {item.actionLabel}
              </button>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}
```

### Checklist Data

```typescript
// lib/config/onboarding-checklist.ts

export const getOnboardingChecklist = (
  userState: UserOnboardingState,
  lang: "ar" | "en",
): ChecklistItem[] => {
  const items = {
    en: [
      {
        id: "profile",
        title: "Complete your profile",
        description: "Add your clinic details and contact information",
        completed: userState.profileComplete,
        actionLabel: "Complete Profile",
        route: "/dashboard/settings/profile",
      },
      {
        id: "whatsapp",
        title: "Connect WhatsApp Business",
        description: "Link your WhatsApp Business account for messaging",
        completed: userState.whatsappConnected,
        actionLabel: "Connect",
        route: "/dashboard/integrations/whatsapp",
      },
      {
        id: "service",
        title: "Add your first service",
        description: "Create a bookable service for your customers",
        completed: userState.hasServices,
        actionLabel: "Add Service",
        route: "/dashboard/services/new",
      },
      {
        id: "team",
        title: "Invite team members",
        description: "Add staff who will manage appointments",
        completed: userState.hasTeamMembers,
        actionLabel: "Invite",
        route: "/dashboard/team/invite",
      },
      {
        id: "test",
        title: "Book a test appointment",
        description: "Try the booking flow as a customer would",
        completed: userState.hasTestBooking,
        actionLabel: "Test Booking",
        route: "/dashboard/demo",
      },
    ],
    ar: [
      {
        id: "profile",
        title: "أكمل ملفك الشخصي",
        description: "أضف تفاصيل عيادتك ومعلومات الاتصال",
        completed: userState.profileComplete,
        actionLabel: "إكمال الملف",
        route: "/dashboard/settings/profile",
      },
      // ... Arabic translations
    ],
  };

  return items[lang];
};
```

## Empty States

### Empty State Component

```tsx
// components/dashboard/EmptyState.tsx

interface EmptyStateProps {
  icon: React.ReactNode;
  title: string;
  description: string;
  actionLabel: string;
  onAction: () => void;
}

export function EmptyState({
  icon,
  title,
  description,
  actionLabel,
  onAction,
}: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center p-12 text-center">
      <div className="w-16 h-16 bg-mawidi-green/10 rounded-full flex items-center justify-center mb-4 text-mawidi-green">
        {icon}
      </div>
      <h3 className="text-xl font-semibold mb-2">{title}</h3>
      <p className="text-gray-500 mb-6 max-w-md">{description}</p>
      <button
        onClick={onAction}
        className="px-6 py-3 bg-mawidi-green text-white rounded-lg hover:bg-mawidi-green-hover"
      >
        {actionLabel}
      </button>
    </div>
  );
}

// Usage
<EmptyState
  icon={<Calendar className="w-8 h-8" />}
  title="No appointments yet"
  description="Your appointments will appear here once customers start booking."
  actionLabel="Share booking link"
  onAction={() => router.push("/dashboard/share")}
/>;
```

## Contextual Help (Tooltips)

```tsx
// components/ui/HelpTooltip.tsx

import { Tooltip } from "@/components/ui/tooltip";

interface HelpTooltipProps {
  content: string;
  children?: React.ReactNode;
}

export function HelpTooltip({ content, children }: HelpTooltipProps) {
  return (
    <Tooltip content={content}>
      <button className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-100 text-gray-500 hover:bg-gray-200">
        <span className="sr-only">Help</span>?
      </button>
    </Tooltip>
  );
}

// Usage
<label className="flex items-center gap-2">
  Service Duration
  <HelpTooltip content="How long does this service typically take? This helps customers plan their schedule." />
</label>;
```

## First-Use Tours

```tsx
// components/onboarding/ProductTour.tsx

import { useState, useEffect } from "react";
import Joyride, { Step } from "react-joyride";

const tourSteps: Step[] = [
  {
    target: '[data-tour="sidebar"]',
    content: "Navigate between different sections using the sidebar",
    disableBeacon: true,
  },
  {
    target: '[data-tour="appointments"]',
    content: "View and manage all your appointments here",
  },
  {
    target: '[data-tour="new-booking"]',
    content: "Create new appointments for walk-in customers",
  },
  {
    target: '[data-tour="share"]',
    content: "Share your booking link with customers via WhatsApp or email",
  },
];

export function ProductTour({
  show,
  onComplete,
}: {
  show: boolean;
  onComplete: () => void;
}) {
  return (
    <Joyride
      steps={tourSteps}
      run={show}
      continuous
      showProgress
      showSkipButton
      callback={({ status }) => {
        if (status === "finished" || status === "skipped") {
          onComplete();
        }
      }}
      styles={{
        options: {
          primaryColor: "#10B981", // mawidi-green
        },
      }}
    />
  );
}
```

## Onboarding Analytics

```typescript
// lib/analytics/onboarding.ts

export const trackOnboardingEvent = (
  event:
    | "signup_started"
    | "signup_completed"
    | "checklist_item_completed"
    | "tour_completed",
  properties?: Record<string, unknown>,
) => {
  // Track to analytics
  analytics.track(event, {
    ...properties,
    timestamp: new Date().toISOString(),
  });
};

// Usage
trackOnboardingEvent("checklist_item_completed", {
  item: "whatsapp_connected",
  timeToComplete: 120, // seconds
});
```

## Quick Reference

### Onboarding Metrics to Track

| Metric                 | Description               | Target    |
| ---------------------- | ------------------------- | --------- |
| Signup completion rate | Users who finish signup   | >80%      |
| Time to first value    | Time to first booking     | <24 hours |
| Checklist completion   | Users finishing all items | >60%      |
| Activation rate        | Users with 3+ bookings    | >40%      |

### Best Practices

- Keep signup under 3 minutes
- Show progress at every step
- Celebrate completions
- Provide skip options (don't block)
- Send reminder emails for incomplete onboarding
- Track drop-off points

## See Also

- [frontend-design](../frontend-design/SKILL.md) - UI design patterns
- [brand-guidelines](../brand-guidelines/SKILL.md) - Mawidi styling
- [i18n-management](../i18n-management/SKILL.md) - Bilingual content
