Elements

Loader

A pixel matrix that keeps time while the model has nothing to show yet.

Generating
fig. 01

Installation

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

Nine cells cycle through a moving band while a label underneath names what is happening, filling the gap before any real content exists. With a runtime you mount it only for that gap and drive its clock yourself; standalone you own the tick and the label outright.

Getting started

Nothing in the runtime ticks a clock for you, and no selector hands you a status string. What the runtime does give you is the one fact that decides whether this belongs on screen at all: a run is active and the newest message has not produced a single part yet.

Gate it on an empty run

components/assistant-ui/elements/loading-state.tsx
"use client";

import { useAuiState } from "@assistant-ui/react";

function useIsAwaitingFirstToken() {
  return useAuiState((s) => {
    if (!s.thread.isRunning) return false;
    const last = s.thread.messages.at(-1);
    return last?.role === "assistant" && last.parts.length === 0;
  });
}

s.thread.messages is the branch currently on screen, so checking its last entry is enough; you do not need a message scope to ask "is anyone waiting on the model right now".

Drive the tick locally

import { useEffect, useState } from "react";
import { GenerationLoader } from "@/components/assistant-ui/elements/loading-state";

function useTick(active: boolean) {
  const [tick, setTick] = useState(0);
  useEffect(() => {
    if (!active) return;
    const id = setInterval(() => setTick((t) => t + 1), 120);
    return () => clearInterval(id);
  }, [active]);
  return tick;
}

function AwaitingFirstToken() {
  const waiting = useIsAwaitingFirstToken();
  const tick = useTick(waiting);
  if (!waiting) return null;
  return <GenerationLoader label="Generating" tick={tick} />;
}

Place AwaitingFirstToken wherever the assistant's next message will render. Once a part arrives, waiting flips to false, the interval clears, and your real content takes over.

Examples

Cell shapes

variant only changes the corner radius of the nine cells: "dots" rounds them into circles, "squares" keeps sharp corners, "rounded" sits between the two.

<GenerationLoader label="Generating" tick={tick} variant="squares" />

Restyle the grid

The cells paint with a flat bg-foreground at two opacity levels (lit and dim); there is no color prop, so retinting the grid means overriding that utility through className or wrapping the element in a text-*/color scope your own CSS reads. The label alone uses the shared ShimmerLabel treatment from surfaces.tsx.

<GenerationLoader
  label="Generating"
  tick={tick}
  className="[&_[aria-hidden]_span]:bg-blue-500"
/>

API reference

Thread state

SelectorTypeDescription
s.thread.isRunningbooleanWhether a run is active.
s.thread.messagesreadonly MessageState[]The branch on screen; .at(-1) is the newest message, whose role and parts.length decide whether the assistant has produced anything visible yet.