React UI
Usage patterns
Common ways to compose @anvia/react-ui with @anvia/react and application-owned UI.
Pair controllers with providers
@anvia/react-ui expects controllers from @anvia/react.
- Use
useChat(...)withChatProvider,Thread,Message,Composer, andHumanInput. - Use
useCompletion(...)withCompletionProviderandCompletion. - Keep server routes, auth, persistence, and model selection outside the UI package.
import { useChat } from "@anvia/react";
import { ChatProvider, Composer, Thread } from "@anvia/react-ui";
const chat = useChat({ endpoint: "http://localhost:8787/api/chat" });
return (
<ChatProvider controller={chat}>
<Thread.Root>
<Thread.Viewport>
<Thread.Messages />
</Thread.Viewport>
<Composer.Root>
<Composer.Input />
<Composer.Submit />
</Composer.Root>
</Thread.Root>
</ChatProvider>
);
The API app route behind http://localhost:8787/api/chat should accept the React wire shape
described in Server routes.
Stay headless
Every primitive accepts regular element props such as className. Button-like primitives also
support asChild, so applications can attach Anvia behavior to design-system components.
<Composer.Submit asChild>
<IconButton aria-label="Send message" icon={<ArrowUpIcon />} />
</Composer.Submit>
Use data-anvia-*, data-role, and data-state selectors when styling should follow primitive
state instead of component names.
[data-anvia-message][data-role="user"] {
justify-items: end;
}
[data-anvia-submit][data-state="disabled"] {
opacity: 0.5;
}
Start default, then specialize
The default message renderer is useful longer than it first looks. Keep it for ordinary parts, and branch only for product-specific tools, attachment layouts, or Markdown components.
import { Message, Thread } from "@anvia/react-ui";
export function Messages() {
return (
<Thread.Messages className="messages">
{(message) => (
<Message.Root className="message">
<Message.Content className="message-content">
<Message.Parts>
{(part) => {
if (part.type === "tool" && part.toolName === "searchDocs") {
return <SearchDocsTool part={part} />;
}
if (part.type === "attachment") {
return (
<Message.Part className="attachment-part">
<Message.Attachment className="attachment-preview" />
</Message.Part>
);
}
return <Message.Part />;
}}
</Message.Parts>
</Message.Content>
</Message.Root>
)}
</Thread.Messages>
);
}.messages {
width: min(760px, 100%);
margin: 0 auto;
display: grid;
gap: 16px;
}
.message {
display: grid;
gap: 8px;
}
.message[data-role="user"] {
justify-items: end;
}
.attachment-preview {
width: min(520px, 100%);
border: 1px solid #3f3f46;
border-radius: 10px;
padding: 12px;
}import { Message, Thread } from "@anvia/react-ui";
export function Messages() {
return (
<Thread.Messages className="mx-auto grid w-full max-w-3xl gap-4">
{(message) => (
<Message.Root className="grid gap-2 data-[role=user]:justify-items-end">
<Message.Content>
<Message.Parts>
{(part) => {
if (part.type === "tool" && part.toolName === "searchDocs") {
return <SearchDocsTool part={part} />;
}
if (part.type === "attachment") {
return (
<Message.Part>
<Message.Attachment className="w-full max-w-xl rounded-lg border border-zinc-700 p-3" />
</Message.Part>
);
}
return <Message.Part />;
}}
</Message.Parts>
</Message.Content>
</Message.Root>
)}
</Thread.Messages>
);
}Attach files in the composer
Composer.AddAttachment opens a file picker, Composer.AttachmentInput gives direct access to the
underlying input, Composer.AttachmentDropzone accepts dropped files, and Composer.Attachments
renders pending attachments before the prompt is sent.
import { Composer } from "@anvia/react-ui";
export function AttachmentComposer() {
return (
<Composer.Root className="composer">
<Composer.AttachmentDropzone className="dropzone">
<Composer.Attachments className="attachment-list" />
<Composer.AddAttachment accept="image/*,.pdf" multiple>
Attach
</Composer.AddAttachment>
<Composer.Input minRows={1} maxRows={6} />
<Composer.Submit />
</Composer.AttachmentDropzone>
</Composer.Root>
);
}.dropzone {
display: grid;
gap: 10px;
}
.dropzone[data-dragging] {
outline: 2px solid #2bf563;
outline-offset: 4px;
}
.attachment-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}import { Composer } from "@anvia/react-ui";
export function AttachmentComposer() {
return (
<Composer.Root className="composer">
<Composer.AttachmentDropzone className="grid gap-2.5 data-[dragging]:outline data-[dragging]:outline-2 data-[dragging]:outline-offset-4 data-[dragging]:outline-[#2BF563]">
<Composer.Attachments className="flex flex-wrap gap-2" />
<Composer.AddAttachment accept="image/*,.pdf" multiple>
Attach
</Composer.AddAttachment>
<Composer.Input minRows={1} maxRows={6} />
<Composer.Submit />
</Composer.AttachmentDropzone>
</Composer.Root>
);
}Add app-owned controls
Keep model selectors, reasoning effort, temperature, provider choice, and similar controls in
application state. Render them inside Composer.Root when they belong in the prompt bar, then pass
their values through useChat({ createRequest }). Use Composer.Root submitMessage only when the
message payload itself needs customization, such as per-message metadata.
See Composer controls for a complete recipe.
const chat = useChat({
endpoint: "http://localhost:8787/api/chat",
createRequest: ({ coreMessages }) => ({
messages: coreMessages,
stream: true,
metadata: {
model,
reasoningEffort,
},
}),
});
Control composer state
Keep the composer uncontrolled for simple chat surfaces. Use controlled props when the draft or pending attachments need to sync with application state.
const [draft, setDraft] = useState("");
const [attachments, setAttachments] = useState<UIAttachment[]>([]);
return (
<Composer.Root
input={draft}
onInputChange={setDraft}
attachments={attachments}
onAttachmentsChange={setAttachments}
>
<Composer.Attachments keepMounted />
<Composer.AttachmentInput multiple />
<Composer.Input />
<Composer.Submit />
</Composer.Root>
);
For restored conversations and server history, see Persistence.
Control empty wrappers
Some collection primitives unmount when empty so they do not create layout gaps. Use keepMounted
when your app CSS needs a stable wrapper.
<Thread.Messages keepMounted={false} />
<Thread.Suggestions keepMounted />
<Composer.Attachments keepMounted />
<HumanInput.Approvals keepMounted />
<HumanInput.Questions keepMounted />