# Onboarding

> A onboarding block.

A shadcn/ui block from the [@blocks-so](https://blocks.so) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/blocks-so/onboarding-01).

- Page, with a live preview: https://shadcn.exchange/r/blocks-so/onboarding-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/blocks-so/onboarding-01-light-78d943f5f88e376c.png
- Registry item: https://blocks.so/r/onboarding-01.json
- Categories: onboarding
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://blocks.so/r/onboarding-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tabler/icons-react`
- Registry items: `button`, `collapsible`, `dropdown-menu`

## Files

### `content/components/onboarding/onboarding-01.tsx`

```tsx
'use client';

import {
  IconArchive,
  IconChevronRight,
  IconCircleCheckFilled,
  IconCircleDashed,
  IconDots,
  IconMail,
} from '@tabler/icons-react';
import { useState } from 'react';
import { Button, buttonVariants } from '@/components/ui/button';
import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible';
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { cn } from '@/lib/utils';

const steps = [
  {
    id: 'profile',
    title: 'Complete your profile',
    description:
      'Add your name, photo, and role so your team knows who you are.',
    completed: true,
    actionLabel: 'Edit profile',
    actionHref: '#',
  },
  {
    id: 'workspace',
    title: 'Set up your workspace',
    description:
      'Customize your workspace with a name, icon, and default settings.',
    completed: false,
    actionLabel: 'Configure workspace',
    actionHref: '#',
  },
  {
    id: 'invite',
    title: 'Invite your team',
    description:
      'Bring your teammates on board so you can collaborate in real time.',
    completed: false,
    actionLabel: 'Send invites',
    actionHref: '#',
  },
  {
    id: 'integrations',
    title: 'Connect integrations',
    description:
      'Link your favorite tools like Slack, GitHub, and Linear to streamline workflows.',
    completed: false,
    actionLabel: 'Browse integrations',
    actionHref: '#',
  },
  {
    id: 'workflow',
    title: 'Create your first workflow',
    description:
      'Automate a repetitive task with a simple drag-and-drop workflow builder.',
    completed: false,
    actionLabel: 'Build workflow',
    actionHref: '#',
  },
  {
    id: 'notifications',
    title: 'Set up notifications',
    description:
      'Choose how and when you want to be notified about updates and mentions.',
    completed: false,
    actionLabel: 'Manage notifications',
    actionHref: '#',
  },
];

interface OnboardingStep {
  id: string;
  title: string;
  description: string;
  completed: boolean;
  actionLabel: string;
  actionHref: string;
}

function CircularProgress({
  completed,
  total,
}: {
  completed: number;
  total: number;
}) {
  const progress = total > 0 ? ((total - completed) / total) * 100 : 0;
  const strokeDashoffset = 100 - progress;

  return (
    <svg className="-rotate-90" height="14" viewBox="0 0 14 14" width="14">
      <circle
        className="stroke-muted"
        cx="7"
        cy="7"
        fill="none"
        pathLength="100"
        r="6"
        strokeWidth="2"
      />
      <circle
        className="stroke-primary"
        cx="7"
        cy="7"
        fill="none"
        pathLength="100"
        r="6"
        strokeDasharray="100"
        strokeLinecap="round"
        strokeWidth="2"
        style={{ strokeDashoffset }}
      />
    </svg>
  );
}

function StepIndicator({ completed }: { completed: boolean }) {
  if (completed) {
    return (
      <IconCircleCheckFilled
        aria-hidden="true"
        className="mt-1 size-4.5 shrink-0 text-primary"
      />
    );
  }
  return (
    <IconCircleDashed
      aria-hidden="true"
      className="mt-1 size-5 shrink-0 stroke-muted-foreground/40"
      strokeWidth={2}
    />
  );
}

export function Onboarding01() {
  const [currentSteps, setCurrentSteps] = useState<OnboardingStep[]>(steps);
  const [openStepId, setOpenStepId] = useState<string | null>(() => {
    const firstIncomplete = steps.find((s) => !s.completed);
    return firstIncomplete?.id ?? steps[0]?.id ?? null;
  });
  const [dismissed, setDismissed] = useState(false);

  const completedCount = currentSteps.filter((s) => s.completed).length;
  const remainingCount = currentSteps.length - completedCount;

  const handleStepClick = (stepId: string) => {
    setOpenStepId(openStepId === stepId ? null : stepId);
  };

  const handleStepAction = (step: OnboardingStep) => {
    const updated = currentSteps.map((s) =>
      s.id === step.id ? { ...s, completed: true } : s
    );
    setCurrentSteps(updated);
    const nextIncomplete = updated.find((s) => !s.completed);
    setOpenStepId(nextIncomplete?.id ?? null);
  };

  if (dismissed) {
    return (
      <div className="flex min-h-dvh items-center justify-center bg-background p-4">
        <div className="text-center">
          <p className="text-pretty text-muted-foreground">
            Checklist dismissed
          </p>
          <button
            className="mt-2 text-primary text-sm underline"
            onClick={() => setDismissed(false)}
          >
            Show again
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="flex min-h-dvh items-center justify-center bg-background p-4">
      <div className="w-full max-w-lg">
        <div className="w-md rounded-lg border bg-card p-4 text-card-foreground shadow-xs">
          <div className="mr-2 mb-4 flex flex-col justify-between sm:flex-row sm:items-center">
            <h3 className="ml-2 text-balance font-semibold text-foreground">
              Get started with Acme
            </h3>
            <div className="mt-2 flex items-center justify-end sm:mt-0">
              <CircularProgress
                completed={remainingCount}
                total={currentSteps.length}
              />
              <div className="mr-3 ml-1.5 text-muted-foreground text-sm">
                <span className="font-medium text-foreground">
                  {completedCount}
                </span>
                {' / '}
                <span className="font-medium text-foreground">
                  {currentSteps.length}
                </span>{' '}
                completed
              </div>
              <DropdownMenu>
                <DropdownMenuTrigger
                  render={
                    <Button className="h-6 w-6" size="icon" variant="ghost" />
                  }
                >
                  <IconDots aria-hidden="true" className="h-4 w-4 shrink-0" />
                  <span className="sr-only">Options</span>
                </DropdownMenuTrigger>
                <DropdownMenuContent align="end" className="w-40">
                  <DropdownMenuItem onClick={() => setDismissed(true)}>
                    <IconArchive
                      aria-hidden="true"
                      className="mr-2 h-4 w-4 shrink-0"
                    />
                    Dismiss
                  </DropdownMenuItem>
                  <DropdownMenuItem
                    onClick={() =>
                      window.open('mailto:support@acme.com?subject=Feedback')
                    }
                  >
                    <IconMail
                      aria-hidden="true"
                      className="mr-2 h-4 w-4 shrink-0"
                    />
                    Give feedback
                  </DropdownMenuItem>
                </DropdownMenuContent>
              </DropdownMenu>
            </div>
          </div>

          <div className="space-y-0">
            {currentSteps.map((step, index) => {
              const isOpen = openStepId === step.id;
              const isFirst = index === 0;
              const prevStep = currentSteps[index - 1];
              const isPrevOpen = prevStep && openStepId === prevStep.id;

              const showBorderTop = !(isFirst || isOpen || isPrevOpen);

              return (
                <div
                  className={cn(
                    'group',
                    isOpen && 'rounded-lg',
                    showBorderTop && 'border-border border-t'
                  )}
                  key={step.id}
                >
                  <div
                    className={cn(
                      'block w-full cursor-pointer text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
                      isOpen && 'rounded-lg'
                    )}
                    onClick={() => handleStepClick(step.id)}
                    onKeyDown={(e) => {
                      if (e.key === 'Enter' || e.key === ' ') {
                        e.preventDefault();
                        handleStepClick(step.id);
                      }
                    }}
                    role="button"
                    tabIndex={0}
                  >
                    <div
                      className={cn(
                        'relative overflow-hidden rounded-lg transition-colors',
                        isOpen && 'border border-border bg-muted'
                      )}
                    >
                      <div className="relative flex items-center justify-between gap-3 py-3 pr-2 pl-4">
                        <div className="flex w-full gap-3">
                          <div className="shrink-0">
                            <StepIndicator completed={step.completed} />
                          </div>
                          <div className="mt-0.5 grow">
                            <h4
                              className={cn(
                                'font-semibold',
                                step.completed
                                  ? 'text-primary'
                                  : 'text-foreground'
                              )}
                            >
                              {step.title}
                            </h4>
                            <Collapsible open={isOpen}>
                              <CollapsibleContent>
                                <p className="mt-2 text-pretty text-muted-foreground text-sm sm:max-w-64 md:max-w-xs">
                                  {step.description}
                                </p>
                                <a
                                  className={cn(
                                    buttonVariants({ size: 'sm' }),
                                    'mt-3'
                                  )}
                                  href={step.actionHref}
                                  onClick={(e) => {
                                    e.stopPropagation();
                                    handleStepAction(step);
                                  }}
                                >
                                  {step.actionLabel}
                                </a>
                              </CollapsibleContent>
                            </Collapsible>
                          </div>
                        </div>
                        {!isOpen && (
                          <IconChevronRight
                            aria-hidden="true"
                            className="h-4 w-4 shrink-0 text-muted-foreground"
                          />
                        )}
                      </div>
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}
```
