Chat
The layout shell for a full-screen chat interface. It puts a fixed header and composer at the edges and gives the transcript the remaining height, so the scroller has a bounded parent to fill.
Support assistant
Usage
import {
Chat,
ChatFooter,
ChatHeader,
ChatMessages,
} from "@e-infra/design-system"
<Chat>
<ChatHeader>
<H4 className="m-0">Support assistant</H4>
</ChatHeader>
<ChatMessages>
<MessageScrollerProvider>{/* … */}</MessageScrollerProvider>
</ChatMessages>
<ChatFooter>
<MessageInput onSubmit={send}>{/* … */}</MessageInput>
</ChatFooter>
</Chat>ChatHeader is optional. ChatMessages takes the remaining height and is already min-h-0, which is what lets the scroller inside it scroll rather than stretch its parent.
Sizing
Chat defaults to h-dvh for a standalone chat route. Override the height when embedding it inside your own layout.
| Context | Class |
|---|---|
| A dedicated chat page | default (h-dvh) |
Inside Header + Content or a Sidebar | className="h-full" |
| A bounded panel | className="h-[32rem]" |
<Content>
<Chat className="h-full">{/* … */}</Chat>
</Content>Whatever height you choose, the ancestor chain must be height-constrained too — a h-full inside an auto-height parent has nothing to fill.
Reply styles
The shell is indifferent to how a turn looks, and there are two conventions worth knowing. Both use the same header, transcript, and composer — only the assistant's turn differs.
Plain text. The reply is full-width prose with no bubble chrome, and carries a copy action. Reads better for long or structured answers, and matches what people expect from an assistant.
Support assistant
Bubbles. The reply sits in a muted bubble beside an avatar. Reads as a conversation between two parties, which suits short exchanges and human-to-human support.
Support assistant
| Plain text | Bubbles | |
|---|---|---|
| Assistant bubble | variant="ghost" | variant="muted" |
| Assistant avatar | omitted | shown |
| Body size | text-base | inherits text-sm |
| Reply actions | supported | supported |
| Best for | long or structured answers | short exchanges, human agents |
In both cases the person's own turn stays a right-aligned bubble. That asymmetry is the point of the plain style — it separates "what I asked" from "what came back".
{/* plain text — the assistant look */}
<Message align="start">
<MessageContent>
<Bubble variant="ghost">
<BubbleContent className="text-base">{reply}</BubbleContent>
</Bubble>
<MessageActions>
<MessageCopyButton value={reply} />
</MessageActions>
</MessageContent>
</Message>
{/* bubbles — the messaging look */}
<Message align="start">
<MessageAvatar>
<Avatar>
<AvatarFallback>AI</AvatarFallback>
</Avatar>
</MessageAvatar>
<MessageContent>
<Bubble variant="muted">
<BubbleContent>{reply}</BubbleContent>
</Bubble>
</MessageContent>
</Message>Whichever you pick, match the typing indicator to it — a bubbled indicator followed by a plain reply makes the answer appear to jump.
Reply actions
Actions are not tied to a reply style. MessageActions reveals on hover of the message, not of the bubble, and aligns itself from the Message alignment — so the same row works under a ghost reply, a bubbled reply, or the person's own turn, with no changes.
Support assistant
It is an open row: put whatever the product needs in it. Match the copy button with variant="ghost" size="icon-sm", and give every icon-only control a visually hidden label.
const replyActions = (reply: string) => (
<MessageActions>
<MessageCopyButton value={reply} />
<Button variant="ghost" size="icon-sm" onClick={() => regenerate(reply)}>
<RefreshCw />
<span className="sr-only">Regenerate reply</span>
</Button>
</MessageActions>
)
{/* the same row, either style */}
<MessageContent>
<Bubble variant={plain ? "ghost" : "muted"}>
<BubbleContent className={plain ? "text-base" : undefined}>{reply}</BubbleContent>
</Bubble>
{replyActions(reply)}
</MessageContent>Lifting the row into a function of the reply, as above, is what keeps the two styles from drifting — the actions are defined once and the style only decides the bubble.
See Message Actions for the copy behaviour and reveal rules, and the Compounds/Chat stories in Storybook for every combination.
Centering the conversation
The shell spans the full width. Constrain the transcript and the composer to the same max width so they line up.
<ChatMessages>
<MessageScrollerProvider>
<MessageScroller>
<MessageScrollerViewport>
<MessageScrollerContent className="mx-auto w-full max-w-3xl p-4">
{/* … */}
</MessageScrollerContent>
</MessageScrollerViewport>
<MessageScrollerButton />
</MessageScroller>
</MessageScrollerProvider>
</ChatMessages>
<ChatFooter>
<MessageInput className="mx-auto max-w-3xl">{/* … */}</MessageInput>
</ChatFooter>Components
| Component | Description |
|---|---|
Chat | Full-height flex column |
ChatHeader | Fixed top bar with a bottom border. Optional. |
ChatMessages | Flexible middle region that hosts the scroller |
ChatFooter | Fixed bottom region with a top border, for the composer |
Props
All four accept native div props plus className.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | - | Additional CSS classes |
| ...props | React.HTMLAttributes | - | Native div props |
Related
Message Scroller for the transcript, Message Input for the composer, and Message with Bubble for the turns themselves.