# Onboarding checklist

> The organization's getting-started steps with progress, hidden once every step is done.

Source: https://bettersupabase.com/docs/registry/ui-recipes/onboarding-checklist

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

Both sides call `useOnboarding` from `better-supabase/blocks/onboarding/react`; the [onboarding block](https://bettersupabase.com/blocks/onboarding) must be installed (`npx better-supabase add onboarding`). Its page on bettersupabase.com is [/recipes/onboarding-checklist](https://bettersupabase.com/recipes/onboarding-checklist).

Web, with shadcn components:

```bash
npx shadcn add button card progress skeleton
```

```bash
npx shadcn add @better-supabase/onboarding-checklist
```

```tsx title="apps/examples/nextjs/src/features/dashboard/components/onboarding-card.tsx"
"use client";

import { useOnboarding } from "better-supabase/blocks/onboarding/react";
import { tenantOf } from "better-supabase/react";
import { CheckCircle2Icon, CircleIcon } from "lucide-react";
import { useExtracted } from "next-intl";

import { buttonVariants } from "@/components/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Skeleton } from "@/components/ui/skeleton";
import { gettingStarted } from "@/features/onboarding/checklist";
import { Link } from "@/i18n/navigation";
import { useAuth } from "@/lib/hooks";
import { blockConfig } from "@/lib/supabase/blocks.generated";

/** The API schema `gen` wrote for the onboarding block. */
const API_SCHEMA = blockConfig.onboarding.api;

/** The organization's getting-started checklist; hidden once every step is done. */
export function OnboardingCard() {
  const t = useExtracted("dashboard");
  const organizationId = tenantOf(useAuth()) ?? null;

  const { progress } = useOnboarding(gettingStarted, {
    organizationId,
    schema: API_SCHEMA,
  });

  if (!organizationId) {
    return null;
  }

  if (progress === undefined) {
    return <OnboardingCardSkeleton />;
  }

  if (progress.done) {
    return null;
  }

  const titles = {
    customer: t("Add your first customer"),
    invite: t("Invite a teammate"),
    "api-key": t("Create an API key"),
    plan: t("Review your plan"),
  };

  return (
    <Card data-testid="onboarding">
      <CardHeader>
        <CardTitle>{t("Get started")}</CardTitle>
        <CardDescription>
          {t("{completed, number} of {total, number} steps done", {
            completed: progress.completed,
            total: progress.total,
          })}
        </CardDescription>
      </CardHeader>
      <CardContent className="space-y-4">
        <Progress value={(progress.completed / progress.total) * 100} />
        <ul className="divide-y">
          {progress.steps.map((step) => (
            <li key={step.id} className="flex items-center gap-3 py-2">
              {step.completed ? (
                <CheckCircle2Icon className="size-4 text-primary" />
              ) : (
                <CircleIcon className="size-4 text-muted-foreground" />
              )}
              <span
                className={
                  step.completed
                    ? "flex-1 text-muted-foreground line-through"
                    : "flex-1"
                }
              >
                {titles[step.id]}
              </span>
              {step.completed ? null : (
                <Link
                  href={step.href ?? "/"}
                  className={buttonVariants({ variant: "outline", size: "sm" })}
                >
                  {t("Start")}
                </Link>
              )}
            </li>
          ))}
        </ul>
      </CardContent>
    </Card>
  );
}

export function OnboardingCardSkeleton() {
  return <Skeleton className="h-64 rounded-xl" aria-busy="true" />;
}
```

React Native, with `ActivityIndicator`, `Text`, `View` from `react-native`:

```bash
npx shadcn add @better-supabase/onboarding-checklist
```

```tsx title="apps/examples/expo-powersync/src/components/onboarding-checklist.tsx"
import { useOnboarding } from "better-supabase/blocks/onboarding/react";
import { tenantOf, useAuth } from "better-supabase/react";
import { ActivityIndicator, Text, View } from "react-native";

import { API_SCHEMA } from "../lib/blocks";
import { gettingStarted } from "../lib/onboarding";

/** The organization's getting-started checklist; hidden once every step is done. */
export function OnboardingChecklist() {
  const organizationId = tenantOf(useAuth()) ?? null;

  const { progress } = useOnboarding(gettingStarted, {
    organizationId,
    schema: API_SCHEMA,
  });

  if (!organizationId) {
    return null;
  }

  if (progress === undefined) {
    return <ActivityIndicator />;
  }

  if (progress.done) {
    return null;
  }

  return (
    <View style={{ gap: 8, padding: 16, borderWidth: 1, borderRadius: 12 }}>
      <Text style={{ fontSize: 17, fontWeight: "600" }}>Get started</Text>
      <Text style={{ color: "#737373" }}>
        {progress.completed} of {progress.total} steps done
      </Text>
      <View style={{ height: 6, borderRadius: 3, backgroundColor: "#e5e5e5" }}>
        <View
          style={{
            height: 6,
            borderRadius: 3,
            backgroundColor: "#111",
            width: `${(progress.completed / progress.total) * 100}%`,
          }}
        />
      </View>
      {progress.steps.map((step) => (
        <Text
          key={step.id}
          style={
            step.completed
              ? { color: "#737373", textDecorationLine: "line-through" }
              : undefined
          }
        >
          {step.completed ? "✓" : "○"} {step.title}
        </Text>
      ))}
    </View>
  );
}
```