Elements

Empty state

The first screen: a greeting, three ways in, and the composer front and center.

What are we building?

Ask anything
fig. 01 · plays once, replay from the corner

Installation

npx shadcn@latest add "@assistant-ui/elements-empty-state"
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.

An empty state is the first screen a thread shows: nothing sent yet, a greeting, a few ways in, and the composer already visible. With a runtime it appears and disappears with the thread's own emptiness; standalone you control when it shows and what it offers.

Getting started

An assistant-ui thread starts empty until the first message lands, and s.thread.isEmpty tracks exactly that.

Show it while the thread is empty

components/assistant-ui/elements/thread.aui.tsx
"use client";

import { AuiIf } from "@assistant-ui/react";
import { EmptyState, EmptyStateGreeting } from "./empty-state";

function Welcome() {
  return (
    <AuiIf condition={(s) => s.thread.isEmpty}>
      <EmptyState>
        <EmptyStateGreeting>How can I help?</EmptyStateGreeting>
        {/* suggestions, composer */}
      </EmptyState>
    </AuiIf>
  );
}

isEmpty stays false while the thread is still loading its history, so a persisted conversation never flashes the greeting first.

Turn suggestions into real prompts

import { useAuiState, ThreadPrimitive } from "@assistant-ui/react";
import { EmptyStateSuggestions, EmptyStateSuggestion } from "./empty-state";

function Suggestions() {
  const suggestions = useAuiState((s) => s.thread.suggestions);
  return (
    <EmptyStateSuggestions>
      {suggestions.map((suggestion, index) => (
        <ThreadPrimitive.Suggestion key={suggestion.prompt} prompt={suggestion.prompt} asChild>
          <EmptyStateSuggestion index={index}>
            {suggestion.title ?? suggestion.prompt}
          </EmptyStateSuggestion>
        </ThreadPrimitive.Suggestion>
      ))}
    </EmptyStateSuggestions>
  );
}

ThreadPrimitive.Suggestion fills the composer with prompt by default, so the reader can edit it before sending; pass send to submit it immediately on click instead. s.thread.suggestions is whatever your app configures on the runtime, static onboarding prompts or ones the model proposed on an earlier turn.

Anatomy

<div data-slot="empty-state">
  <h2 data-slot="empty-state-greeting" />
  <div data-slot="empty-state-suggestions">
    <button data-slot="empty-state-suggestion" />
  </div>
  <div data-slot="empty-state-composer">
    <span>{/* placeholder text */}</span>
    <button aria-label="Send" />
  </div>
</div>

The greeting, the suggestion row, and the composer each fade and slide in on mount at increasing delays, the composer last, at 360ms, so the screen builds top to bottom instead of appearing all at once. EmptyStateComposer is the same shell as ChatPanelComposer: placeholder is displayed text, not a bound value, and the send button disables itself whenever onSend is left undefined.

Examples

Restyle the layout

EmptyState only sets a max width and a vertical gap; every part accepts className, so the row of suggestions can wrap to a grid or the greeting can drop the animation:

<EmptyState className="max-w-lg gap-4">
  <EmptyStateGreeting className="animate-none text-3xl">
    How can I help?
  </EmptyStateGreeting>
  {/* ... */}
</EmptyState>

Suggestion delay

The stagger comes entirely from the index you pass; a filtered or reordered list re-derives it from the array position, not from a stored value:

{suggestions.map((suggestion, index) => (
  <ThreadPrimitive.Suggestion key={suggestion.prompt} prompt={suggestion.prompt} asChild>
    <EmptyStateSuggestion index={index}>{suggestion.prompt}</EmptyStateSuggestion>
  </ThreadPrimitive.Suggestion>
))}

API reference

Primitive parts

PartRendersNotes
AuiIfchildrenRenders children while condition selects true; use (s) => s.thread.isEmpty.
ThreadPrimitive.SuggestionbuttonFills the composer with prompt on click by default; pass send to submit it immediately. Accepts asChild.

Thread state

SelectorTypeDescription
s.thread.isEmptybooleantrue when the thread has no messages and isn't loading history.
s.thread.suggestionsreadonly ThreadSuggestion[]{ title?, label?, prompt } entries configured on the runtime.