Skip to content
Elements

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?
fig. 01

Installation

Install manually
npx shadcn@latest add "@assistant-ui/elements-onboarding"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A short, dismissible tour: one step at a time, each with a line of body copy and a worked example, a progress dot per step, and Skip next to Next. With a runtime the one thing worth wiring is what happens when the tour ends; standalone every transition is a callback you already control.

Getting started

The steps, their copy, and their order are content you write, so there is nothing to read from useAuiState. The runtime is involved only when the tour ends: "Skip" and the last step's "Start" hand off to the thread.

Hand off to the thread when the tour ends

components/assistant-ui/elements/first-run.tsx
"use client";

import { useState } from "react";
import { useAui } from "@assistant-ui/react";
import {
  Onboarding,
  type OnboardingStep,
} from "@/components/assistant-ui/elements/onboarding";

const STEPS: OnboardingStep[] = [
  { title: "Ask anything", body: "Type a question, it answers in place.", example: "Summarize this PDF" },
  { title: "It can act, not just answer", body: "Grant a tool and it can search, run code, or call your API.", example: "Check the weather in Tokyo" },
  { title: "Every answer is a branch", body: "Regenerating never loses the original; step between versions.", example: "n / m stepper on any reply" },
];

export function FirstRun({ onFinish }: { onFinish: () => void }) {
  const aui = useAui();
  const [index, setIndex] = useState(0);
  const last = index >= STEPS.length - 1;

  const finish = () => {
    aui.threads.switchToNewThread();
    onFinish();
  };

  return (
    <Onboarding
      steps={STEPS}
      index={index}
      onNext={() => (last ? finish() : setIndex((i) => i + 1))}
      onSkip={finish}
    />
  );
}

switchToNewThread() only matters if your app defers creating a thread until the tour is dismissed; if the thread and composer already exist behind the overlay, finish only needs to call onFinish().

Anatomy

<div data-slot="onboarding">
  <div>
    {/* keyed on the current step, so it fades in fresh every change */}
    <span>{/* "n of m" */}</span>
    <span>{/* step title */}</span>
    <p>{/* step body */}</p>
    <span>{/* step example */}</span>
  </div>
  <div>
    <span>{/* one dot per step, decorative only */}</span>
    <button>{/* Skip */}</button>
    <button>{/* Next, or Start on the last step */}</button>
  </div>
</div>

With zero steps the element renders nothing. index is floored and clamped into 0…steps.length - 1 before use, so a value outside that range, or a fractional one, still resolves to a real step. The element owns no progression: onNext and onSkip only report the press, so nothing advances until the caller moves index, as in both examples above. The progress dots are aria-hidden and are not buttons, so a user cannot jump to a step by clicking one.

Examples

Adding a way back

index is a controlled prop and the element never advances it, so you can add a back control of your own:

<button type="button" onClick={() => setIndex((i) => Math.max(0, i - 1))}>
  Back
</button>

Restyle the tour

Both lanes take className on the root. The example block sits on the field surface and the Next button on inkButton, so retheming those two tokens covers both accent surfaces.

<Onboarding className="max-w-xs" /* ... */ />

API reference

This element has no dedicated primitive; the only runtime call involved is finishing the tour, as in Getting started.

Threads

MethodTypeDescription
aui.threads.switchToNewThread()() => voidSelects a fresh, unsent thread as the open one. Only needed when thread creation is deferred until onboarding completes.