React UI
Getting started
Install @anvia/react-ui, wire it to useChat, and render a styled chat surface.
Install the UI package beside the React hook package:
pnpm add @anvia/react @anvia/react-ui
If you do not already have a React app and API route, start with TanStack quickstart. This page focuses on the UI composition once a compatible API endpoint exists.
Route requirement
The endpoint is the API app URL, not a frontend route. For local development:
const chat = useChat({ endpoint: "http://localhost:8787/api/chat" });
That call posts the default React request body:
type UIStreamRequest = {
messages: Message[];
stream: true;
metadata?: JsonValue;
};
The route should read body.messages and return an Anvia stream:
import type { UIStreamRequest } from "@anvia/core";
import { createEventStream } from "@anvia/server";
export async function POST(request: Request) {
const body = (await request.json()) as UIStreamRequest;
return createEventStream(agent.prompt(body.messages).stream(), {
format: "jsonl",
});
}
Build the first chat
@anvia/react owns the useChat(...) controller. @anvia/react-ui reads that controller through
ChatProvider and renders the thread, messages, suggestions, errors, scrolling, attachments, and
composer controls.
import { useChat } from "@anvia/react";
import { ChatProvider, Composer, Thread } from "@anvia/react-ui";
import "@anvia/react-ui/styles.css";
export function SupportChat() {
const chat = useChat({
endpoint: "http://localhost:8787/api/chat",
suggestions: [
{ id: "summarize", label: "Summarize", prompt: "Summarize this thread." },
{ id: "next", label: "Next step", prompt: "What should I do next?" },
],
});
return (
<ChatProvider controller={chat}>
<Thread.Root className="chat">
<Thread.Viewport className="chat-scroll">
<Thread.Empty className="empty-state">Start a conversation.</Thread.Empty>
<Thread.Suggestions className="suggestions" />
<Thread.Messages className="messages" />
<Thread.Error className="thread-error" />
<Thread.ScrollToBottom className="scroll-button">Latest</Thread.ScrollToBottom>
</Thread.Viewport>
<Composer.Root className="composer">
<Composer.Attachments className="composer-attachments" />
<Composer.AddAttachment>Attach</Composer.AddAttachment>
<Composer.Input placeholder="Ask a question..." maxRows={6} />
<Composer.Stop>Stop</Composer.Stop>
<Composer.Submit>Send</Composer.Submit>
</Composer.Root>
</Thread.Root>
</ChatProvider>
);
}.chat {
min-height: 100vh;
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
}
.chat-scroll {
min-height: 0;
overflow-y: auto;
padding: 24px 16px;
}
.messages,
.empty-state,
.suggestions,
.thread-error {
width: min(760px, 100%);
margin: 0 auto;
}
.messages {
display: grid;
gap: 16px;
}
.messages [data-anvia-message] {
display: grid;
gap: 8px;
}
.messages [data-role="user"] {
justify-items: end;
}
.composer {
width: min(760px, calc(100% - 32px));
margin: 0 auto 16px;
display: flex;
gap: 8px;
align-items: end;
}import { useChat } from "@anvia/react";
import { ChatProvider, Composer, Thread } from "@anvia/react-ui";
import "@anvia/react-ui/styles.css";
export function SupportChat() {
const chat = useChat({ endpoint: "http://localhost:8787/api/chat" });
return (
<ChatProvider controller={chat}>
<Thread.Root className="grid min-h-screen grid-rows-[minmax(0,1fr)_auto] bg-zinc-950 text-zinc-100">
<Thread.Viewport className="min-h-0 overflow-y-auto px-4 py-6">
<Thread.Empty className="mx-auto w-full max-w-3xl">Start a conversation.</Thread.Empty>
<Thread.Messages
className="
mx-auto grid w-full max-w-3xl gap-4
[&_[data-anvia-message]]:grid
[&_[data-anvia-message]]:gap-2
[&_[data-role=user]]:justify-items-end
"
/>
<Thread.Error className="mx-auto w-full max-w-3xl" />
<Thread.ScrollToBottom className="fixed right-4 bottom-24 rounded-full border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm">
Latest
</Thread.ScrollToBottom>
</Thread.Viewport>
<Composer.Root className="mx-auto mb-4 flex w-[min(760px,calc(100%-32px))] items-end gap-2 rounded-2xl border border-zinc-700 bg-zinc-900 p-2">
<Composer.Attachments className="flex flex-wrap gap-2" />
<Composer.AddAttachment>Attach</Composer.AddAttachment>
<Composer.Input className="min-w-0 flex-1 resize-none bg-transparent outline-none" maxRows={6} />
<Composer.Stop>Stop</Composer.Stop>
<Composer.Submit>Send</Composer.Submit>
</Composer.Root>
</Thread.Root>
</ChatProvider>
);
}What happened
The example uses the default message renderer through <Thread.Messages />. That means the package
renders every supported UIMessagePart for you: text, reasoning, tool calls, attachments, data, and
errors.
The app still owns:
- The API app route at
/api/chat. - The API app origin and CORS policy.
- Authentication and persistence.
- Page layout and final visual styling.
- Custom message, tool-call, or attachment components when the defaults are not enough.
Next steps
- Build the route in Server routes.
- Customize request options in Request shape.
- Learn the renderer levels in Messages.
- Add file inputs and dropzones in Composer attachments.
- Build a complete app-owned surface in Full chat surface.
