React UI
Styling
Style headless Anvia React UI primitives with class names, data attributes, Vanilla CSS, or Tailwind CSS.
@anvia/react-ui is headless. The package owns behavior, state, context, and stable attributes.
Your application owns layout, spacing, color, typography, and design-system integration.
What the package styles
The optional stylesheet is for prototypes and functional defaults:
import "@anvia/react-ui/styles.css";
It keeps rich composer and textarea resizing, disabled states, media bounds, code wrapping, and basic overflow sane. It does not try to create your final chat layout.
What your app styles
Your app should style:
- Page and viewport height.
- Scroll container padding.
- Message list width and gaps.
- User and assistant message alignment.
- Bubbles, cards, tool-call panels, and attachment previews.
- Composer width, controls, and action states.
Chat shell
Start by making the thread and composer behave like one surface.
import { Composer, Thread } from "@anvia/react-ui";
export function ChatShell() {
return (
<Thread.Root className="chat">
<Thread.Viewport className="chat-scroll">
<Thread.Empty className="empty-state">Start a conversation.</Thread.Empty>
<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 maxRows={6} placeholder="Ask a question..." />
<Composer.Stop>Stop</Composer.Stop>
<Composer.Submit>Send</Composer.Submit>
</Composer.Root>
</Thread.Root>
);
}.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 {
width: min(760px, 100%);
margin: 0 auto;
display: grid;
gap: 16px;
}
.empty-state,
.thread-error {
width: min(760px, 100%);
margin: 0 auto;
}import { Composer, Thread } from "@anvia/react-ui";
export function ChatShell() {
return (
<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" />
<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>
);
}Message rows
Use data-role on Message.Root for role-specific alignment and bubbles. This works for both the
default part renderer and custom part renderers.
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 />
</Message.Content>
</Message.Root>
)}
</Thread.Messages>
);
}.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[data-role="assistant"] .message-content {
line-height: 1.7;
}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="group/message grid gap-2 data-[role=user]:justify-items-end">
<Message.Content
className="
max-w-[min(680px,100%)]
group-data-[role=assistant]/message:leading-7
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.Root>
)}
</Thread.Messages>
);
}Composer
Composer primitives emit state attributes, so the submit and stop controls can change based on whether the chat is idle, submitting, or streaming.
import { Composer } from "@anvia/react-ui";
export function ComposerBar() {
return (
<Composer.Root className="composer">
<Composer.Attachments className="composer-attachments" />
<Composer.AddAttachment className="composer-button">Attach</Composer.AddAttachment>
<Composer.Input className="composer-input" maxRows={6} />
<Composer.Stop className="composer-button">Stop</Composer.Stop>
<Composer.Submit className="composer-button composer-submit">Send</Composer.Submit>
</Composer.Root>
);
}.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-radius: 10px;
border: 1px solid #3f3f46;
background: #27272a;
color: #f4f4f5;
padding: 8px 10px;
}
.composer [data-anvia-submit][data-state="disabled"] {
opacity: 0.45;
}import { Composer } from "@anvia/react-ui";
export function ComposerBar() {
return (
<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" 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-semibold text-zinc-950 data-[state=disabled]:opacity-50">
Send
</Composer.Submit>
</Composer.Root>
);
}Data attributes
Use attributes when styling should follow primitive state instead of a component name.
[data-anvia-message][data-role="user"] {
justify-items: end;
}
[data-anvia-tool][data-state="output-available"] {
border-color: #22c55e;
}
[data-anvia-composer][data-state="streaming"] [data-anvia-submit] {
display: none;
}
[data-anvia-scroll-to-bottom][data-state="bottom"] {
visibility: hidden;
}
Common attributes:
| Attribute | Emitted by | Use |
|---|---|---|
data-anvia-thread |
Thread.Root |
Root chat state. |
data-anvia-thread-viewport |
Thread.Viewport |
Scroll container state. |
data-anvia-message |
Message.Root |
Message row styling. |
data-role |
Message.Root |
user, assistant, system, or tool alignment. |
data-anvia-part |
Message.Part |
Per-part layout. |
data-anvia-tool |
Message.Tool |
Tool-call cards. |
data-anvia-composer |
Composer.Root |
Composer state. |
data-anvia-composer-input |
Composer.Input and Composer.TextareaInput |
Prompt input wrapper. |
data-anvia-composer-editor |
Rich editor inside Composer.Input |
Contenteditable editor styling. |
data-anvia-composer-textarea-input |
Composer.TextareaInput |
Native textarea fallback styling. |
data-anvia-composer-entity |
Inline trigger entity | Mention or command chip styling. |
data-anvia-composer-trigger-menu |
Composer.TriggerMenu |
Floating trigger menu. |
data-anvia-composer-trigger-item |
Composer.TriggerItem |
Trigger menu option styling. |
data-anvia-submit |
Composer.Submit |
Submit enabled/disabled state. |
data-anvia-human-input-panel |
HumanInput.Panel |
Review panel layout. |
When using a design system, attach primitive behavior to your component with asChild:
<Composer.Submit asChild>
<Button type="submit" variant="primary">
Send
</Button>
</Composer.Submit>
For a complete copy-paste surface, see Styling recipe.
