React UI
Styling recipe
Application-owned CSS and Tailwind CSS for a practical @anvia/react-ui chat surface.
The optional package stylesheet handles functional defaults. This recipe shows the layout and visual styling that should live in the app.
import { ChatProvider, Composer, Message, Thread } from "@anvia/react-ui";
import { useChat } from "@anvia/react";
import "@anvia/react-ui/styles.css";
export function StyledChat() {
const chat = useChat({ endpoint: "http://localhost:8787/api/chat" });
return (
<ChatProvider controller={chat}>
<Thread.Root className="chat">
<Thread.Viewport className="chat-scroll">
<Thread.Empty className="empty-state">Start a conversation.</Thread.Empty>
<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>Retry</Message.Regenerate>
</Message.Actions>
) : null}
</Message.Root>
)}
</Thread.Messages>
<Thread.ScrollToBottom className="scroll-button">Latest</Thread.ScrollToBottom>
</Thread.Viewport>
<Composer.Root className="composer">
<Composer.Attachments className="attachment-list" />
<Composer.AddAttachment className="composer-button">Attach</Composer.AddAttachment>
<Composer.Input className="composer-input" placeholder="Ask..." maxRows={6} />
<Composer.Stop className="composer-button">Stop</Composer.Stop>
<Composer.Submit className="composer-button 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 {
width: min(760px, 100%);
margin: 0 auto;
}
.messages {
display: grid;
gap: 16px;
}
.message {
display: grid;
gap: 8px;
}
.message[data-role="user"] {
justify-items: end;
}
.message-content {
max-width: min(680px, 100%);
}
.message[data-role="user"] .message-content {
max-width: min(620px, 88%);
border-radius: 18px;
background: #27272a;
padding: 10px 14px;
}
.message-actions {
display: flex;
gap: 6px;
color: #a1a1aa;
}
.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;
}
.composer-input {
min-width: 0;
flex: 1;
border: 0;
outline: 0;
background: transparent;
color: inherit;
}
.composer-button {
border: 1px solid #3f3f46;
border-radius: 10px;
background: #27272a;
color: inherit;
padding: 8px 10px;
}
.composer-submit {
background: #2bf563;
color: #101014;
font-weight: 700;
}import { ChatProvider, Composer, Message, Thread } from "@anvia/react-ui";
import { useChat } from "@anvia/react";
import "@anvia/react-ui/styles.css";
export function StyledChat() {
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">
{(message) => (
<Message.Root className="group/message grid gap-2 data-[role=user]:justify-items-end">
<Message.Content className="max-w-[min(680px,100%)] group-data-[role=user]/message:max-w-[min(620px,88%)] group-data-[role=user]/message:rounded-[18px] group-data-[role=user]/message:bg-zinc-800 group-data-[role=user]/message:px-3.5 group-data-[role=user]/message:py-2.5">
<Message.Parts />
</Message.Content>
{message.role === "assistant" ? (
<Message.Actions className="flex gap-1.5 text-sm text-zinc-400">
<Message.Copy>Copy</Message.Copy>
<Message.Regenerate>Retry</Message.Regenerate>
</Message.Actions>
) : null}
</Message.Root>
)}
</Thread.Messages>
<Thread.ScrollToBottom className="fixed right-4 bottom-24 rounded-full border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm data-[state=bottom]:invisible">
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 className="rounded-lg border border-zinc-700 bg-zinc-800 px-2.5 py-2">Attach</Composer.AddAttachment>
<Composer.Input className="min-w-0 flex-1 resize-none bg-transparent outline-none" placeholder="Ask..." maxRows={6} />
<Composer.Stop className="rounded-lg border border-zinc-700 bg-zinc-800 px-2.5 py-2">Stop</Composer.Stop>
<Composer.Submit className="rounded-lg bg-[#2BF563] px-2.5 py-2 font-bold text-zinc-950">Send</Composer.Submit>
</Composer.Root>
</Thread.Root>
</ChatProvider>
);
}State selectors
Use data-state, data-role, and data-anvia-* selectors for state-specific styling.
[data-anvia-composer][data-state="streaming"] [data-anvia-submit] {
display: none;
}
[data-anvia-scroll-to-bottom][data-state="bottom"] {
visibility: hidden;
}
[data-anvia-tool][data-state="error"] {
border-color: #fb7185;
}
Keep the package stylesheet imported for behavior, then put product layout and visual rules in your app CSS or Tailwind classes.
For a list of stable attributes, see Reference.
