Elements

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

Installation

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 themselves, their copy and their order, are content you write, not state the runtime holds; there is nothing to read from useAuiState here. The one real touchpoint is what "Skip" and the final "Next" actually do: 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 can just call onFinish() and let them take over.

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 at all, not an empty shell; steps[0] being undefined is the signal it checks. 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 instead of rendering blank. The element owns no progression: onNext and onSkip are pure signals, not state changes, so nothing advances until the caller moves index itself, as in both examples above. The progress dots are aria-hidden and purely visual; they are not buttons, so nothing lets a user jump back to an earlier step by clicking one.

Examples

Adding a way back

Since index is a plain controlled prop and the element never advances it, a back control is exactly as valid as onNext, even though nothing built in renders one:

<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 final button on inkButton, so retheming those two tokens covers the card's two accent surfaces together.

<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.