Elements

99 / 122 · Thread

Onboarding

First run: three moves that teach what this assistant is actually for.

1 of 3Ask about your own code

It reads the repo you point it at, so questions can be specific rather than general.

Why does the composer re-render on every keystroke?

Installation

npx shadcn@latest add "@assistant-ui/elements-onboarding"

Usage

import { Onboarding } from "@/components/elements/onboarding";

<Onboarding steps={steps} index={index} onNext={next} onSkip={skip} />

Props

Onboarding

steps*OnboardingStep[]What to teach, in order. Three is usually the limit before it is skipped.
index*numberWhich step is showing. The final step's action reads Start instead of Next.
onNext() => voidCalled to advance.
onSkip() => voidCalled to dismiss. Always offer it.
classNamestringExtra classes merged onto the root.

OnboardingStep

title*stringStep heading.
body*stringWhat this step teaches, in a sentence or two.
example*stringA prompt the user could actually send. Showing beats describing.

Source

onboarding.tsx
"use client";

import { cn } from "@/lib/utils";
import { field, inkButton, mono, paper } from "./surfaces";

export interface OnboardingStep {
  title: string;
  body: string;
  example: string;
}

export function Onboarding({
  steps,
  index,
  onNext,
  onSkip,
  className,
}: {
  steps: readonly OnboardingStep[];
  index: number;
  onNext?: () => void;
  onSkip?: () => void;
  className?: string;
}) {
  const step = steps[Math.min(index, steps.length - 1)];
  if (!step) return null;
  const last = index >= steps.length - 1;

  return (
    <div
      className={cn(
        paper,
        "flex w-full max-w-sm flex-col gap-4 rounded-[20px] p-5",
        className,
      )}
    >
      <div
        key={index}
        className="fade-in animate-in flex flex-col gap-2 duration-300"
      >
        <span className={cn(mono, "text-foreground/30 tabular-nums")}>
          {index + 1} of {steps.length}
        </span>
        <span className="text-[15px] font-medium tracking-tight">
          {step.title}
        </span>
        <p className="text-foreground/55 text-[13px] leading-relaxed">
          {step.body}
        </p>
        <span
          className={cn(
            field,
            "text-foreground/60 rounded-xl px-3 py-2 text-[13px] leading-relaxed",
          )}
        >
          {step.example}
        </span>
      </div>

      <div className="flex items-center gap-2">
        <span className="flex gap-1.5">
          {steps.map((_, i) => (
            <span
              key={i}
              aria-hidden
              className={cn(
                "h-1 rounded-full transition-all duration-300 motion-reduce:transition-none",
                i === index ? "bg-foreground/60 w-4" : "bg-foreground/15 w-1",
              )}
            />
          ))}
        </span>

        <button
          type="button"
          onClick={onSkip}
          className="text-foreground/45 hover:bg-foreground/[0.06] hover:text-foreground/90 ms-auto h-8 rounded-full px-3 text-xs font-medium transition-[background-color,color,scale] duration-150 active:scale-[0.96]"
        >
          Skip
        </button>
        <button
          type="button"
          onClick={onNext}
          className={cn(
            inkButton,
            "flex h-8 items-center rounded-full px-3.5 text-xs font-medium",
          )}
        >
          {last ? "Start" : "Next"}
        </button>
      </div>
    </div>
  );
}