React UI
Full chat surface
Compose a complete @anvia/react-ui chat surface with messages, suggestions, scrolling, and composer controls.
This recipe starts from a useChat(...) controller and builds a complete app-owned chat screen.
It intentionally uses the default message renderer first. Add a custom message row only after you
need custom Markdown, tool cards, attachments, or actions.
The recipe assumes a compatible API app endpoint. If you do not have one yet, build the app from TanStack quickstart or wire the endpoint with Server routes.
import { useChat } from "@anvia/react";
import { ChatProvider, Composer, Thread } from "@anvia/react-ui";
import "@anvia/react-ui/styles.css";
export function AgentChat() {
const chat = useChat({
endpoint: "http://localhost:8787/api/chat",
suggestions: [
{ id: "summarize", label: "Summarize", prompt: "Summarize this conversation." },
{ id: "risks", label: "Find risks", prompt: "What are the risks in this plan?" },
{ id: "next", label: "Next step", prompt: "Suggest the next implementation step." },
],
});
return (
<ChatProvider controller={chat}>
<Thread.Root className="chat">
<Thread.Viewport className="chat-scroll">
<Thread.Empty className="empty-state">
<h1>What should we build?</h1>
<Thread.Suggestions className="suggestions" />
</Thread.Empty>
<Thread.Messages className="messages" />
<Thread.Error className="thread-error" />
<Thread.ScrollToBottom className="scroll-button">Jump to latest</Thread.ScrollToBottom>
</Thread.Viewport>
<Composer.Root className="composer">
<Composer.Attachments className="composer-attachments" />
<Composer.AddAttachment>Attach</Composer.AddAttachment>
<Composer.Input minRows={1} maxRows={6} placeholder="Message the agent..." />
<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;
background: #0f1014;
color: #f4f4f5;
}
.chat-scroll {
min-height: 0;
overflow-y: auto;
padding: 24px 16px;
}
.messages,
.empty-state,
.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;
}
.suggestions {
margin-top: 16px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.composer {
width: min(760px, calc(100% - 32px));
margin: 0 auto 16px;
display: flex;
gap: 8px;
align-items: end;
border: 1px solid #3f3f46;
border-radius: 18px;
background: #18181b;
padding: 8px;
}import { useChat } from "@anvia/react";
import { ChatProvider, Composer, Thread } from "@anvia/react-ui";
import "@anvia/react-ui/styles.css";
export function AgentChat() {
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">
<h1 className="text-2xl font-semibold">What should we build?</h1>
<Thread.Suggestions className="mt-4 flex flex-wrap gap-2" />
</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">
Jump to latest
</Thread.ScrollToBottom>
</Thread.Viewport>
<Composer.Root className="mx-auto mb-4 flex w-[min(760px,calc(100%-32px))] items-end gap-2 rounded-[18px] 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" minRows={1} maxRows={6} />
<Composer.Stop>Stop</Composer.Stop>
<Composer.Submit>Send</Composer.Submit>
</Composer.Root>
</Thread.Root>
</ChatProvider>
);
}Add message actions
If you need copy or regenerate controls, switch Thread.Messages to a child function and keep
Message.Parts as the renderer.
<Thread.Messages className="messages">
{(message) => (
<Message.Root className="message">
<Message.Content className="message-content">
<Message.Parts />
</Message.Content>
{message.role === "assistant" ? (
<Message.Actions className="message-actions">
<Message.Copy>Copy</Message.Copy>
<Message.Regenerate>Regenerate</Message.Regenerate>
</Message.Actions>
) : null}
</Message.Root>
)}
</Thread.Messages>
For deeper message customization, continue to Message rendering.
