Skip to content
UI

Search docs

Search components and documentation

New
← Back to application blocks

onboarding-01

A multi-step onboarding card with a numbered step indicator, a progress bar and per-step form fields, plus Back/Continue navigation. Use as a first-run setup flow.

Code

"use client"

import * as React from "react"
import { CheckIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Progress } from "@/components/ui/progress"

const steps = [
  { title: "Account", description: "Tell us who you are." },
  { title: "Workspace", description: "Name your workspace." },
  { title: "Invite", description: "Bring your team along." },
]

export function Onboarding({ className, ...props }: React.ComponentProps<"div">) {
  const [step, setStep] = React.useState(0)
  const isLast = step === steps.length - 1
  const progress = ((step + 1) / steps.length) * 100
  const current = steps[step]

  return (
    <div
      className={cn("mx-auto flex w-full max-w-md flex-col px-6 py-16", className)}
      {...props}
    >
      <Card>
        <CardHeader>
          <div className="mb-4 flex items-center justify-between">
            {steps.map((s, index) => {
              const done = index < step
              const active = index === step
              return (
                <React.Fragment key={s.title}>
                  <div className="flex flex-col items-center gap-1.5">
                    <div
                      className={cn(
                        "flex size-8 items-center justify-center rounded-full border text-sm font-medium transition-colors",
                        done && "border-primary bg-primary text-primary-foreground",
                        active && "border-primary text-foreground",
                        !done && !active && "text-muted-foreground"
                      )}
                    >
                      {done ? <CheckIcon className="size-4" /> : index + 1}
                    </div>
                    <span
                      className={cn(
                        "text-xs",
                        active ? "text-foreground" : "text-muted-foreground"
                      )}
                    >
                      {s.title}
                    </span>
                  </div>
                  {index < steps.length - 1 && (
                    <div className="mx-2 mb-5 h-px flex-1 bg-border" />
                  )}
                </React.Fragment>
              )
            })}
          </div>
          <Progress
            value={progress}
            aria-label={`Onboarding progress: step ${step + 1} of ${steps.length}`}
            className="h-1.5"
          />
          <CardTitle className="pt-4">{current.title}</CardTitle>
          <CardDescription>{current.description}</CardDescription>
        </CardHeader>
        <CardContent className="grid gap-4">
          {step === 0 && (
            <>
              <div className="grid gap-2">
                <Label htmlFor="name">Full name</Label>
                <Input id="name" placeholder="Ada Lovelace" />
              </div>
              <div className="grid gap-2">
                <Label htmlFor="email">Email</Label>
                <Input id="email" type="email" placeholder="ada@example.com" />
              </div>
            </>
          )}
          {step === 1 && (
            <>
              <div className="grid gap-2">
                <Label htmlFor="workspace">Workspace name</Label>
                <Input id="workspace" placeholder="Acme Inc." />
              </div>
              <div className="grid gap-2">
                <Label htmlFor="url">Workspace URL</Label>
                <Input id="url" placeholder="acme" />
              </div>
            </>
          )}
          {step === 2 && (
            <div className="grid gap-2">
              <Label htmlFor="invite">Invite teammates</Label>
              <Input id="invite" type="email" placeholder="teammate@example.com" />
              <p className="text-muted-foreground text-xs">
                Separate multiple emails with a comma. You can skip this and do it
                later.
              </p>
            </div>
          )}
        </CardContent>
        <CardFooter className="justify-between">
          <Button
            variant="ghost"
            onClick={() => setStep((s) => Math.max(0, s - 1))}
            disabled={step === 0}
          >
            Back
          </Button>
          <Button
            onClick={() => {
              if (!isLast) setStep((s) => s + 1)
            }}
          >
            {isLast ? "Finish" : "Continue"}
          </Button>
        </CardFooter>
      </Card>
    </div>
  )
}