Onboarding
First run: three moves that teach what this assistant is actually for.
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
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 initThen 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.
npx shadcn@latest add "@assistant-ui/elements-onboarding"Props-driven: no runtime or provider required.
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
"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().
Standalone, the tour is entirely local: index is state you hold, and onNext deciding whether to advance or finish is the only logic involved.
Hold the step index
"use client";
import { useState } from "react";
import {
Onboarding,
type OnboardingStep,
} from "@/components/assistant-ui/elements/onboarding";
const steps: OnboardingStep[] = [
{ title: "Welcome", body: "Three quick things before you start.", example: "…" },
{ title: "Ask in plain language", body: "No special syntax needed.", example: "What changed in v4?" },
];
export function Tour({ onDone }: { onDone: () => void }) {
const [index, setIndex] = useState(0);
const last = index >= steps.length - 1;
return (
<Onboarding
steps={steps}
index={index}
onNext={() => (last ? onDone() : setIndex(index + 1))}
onSkip={onDone}
/>
);
}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
| Method | Type | Description |
|---|---|---|
aui.threads.switchToNewThread() | () => void | Selects a fresh, unsent thread as the open one. Only needed when thread creation is deferred until onboarding completes. |
Onboarding
| Prop | Type | Default | Description |
|---|---|---|---|
steps | readonly OnboardingStep[] | required | The tour's steps, in order. An empty array renders nothing. |
index | number | required | The step to show. Floored and clamped into 0…steps.length - 1. |
onNext | () => void | Called when "Next" (or "Start", on the last step) is pressed. Does not change index itself. | |
onSkip | () => void | Called when "Skip" is pressed. | |
className | string | Merged onto the root. |
All other div props are forwarded to the root.
OnboardingStep
| Field | Type | Description |
|---|---|---|
title | string | Step heading. |
body | string | One paragraph of explanation. |
example | string | A worked example shown in its own field below the body. |