Skip to content

Chat

How should I handle state?
Keep the source of truth in one place.

Usage ​

The chat display is a set of pieces that stack into a transcript: a scrolling layout, a message log, rows for each sender, bubbles, tool calls, a reasoning disclosure, a typing line, and an unread rule. Message bodies are slots. This suite does not render Markdown; pass plain text, or bring your own renderer.

ChatLayout is a column. The transcript grows, and the composer stays pinned to the bottom over a frosted scrim. By default the layout itself scrolls. Pass scrollRef when an ancestor should scroll instead, and the dock switches from sticky to fixed.

While the reader is at the bottom, new text keeps the view pinned there. Scrolling up lets go immediately and reveals the scroll-to-bottom button. If messages arrive while the view is unlocked, the button label changes to the localized “New messages” string, a live region announces it, and ChatUnreadDivider can mark the boundary. ChatLayout wires this through useChatStreamScroll and useChatNewMessages. Set showScrollButton to false to hide the built-in button, or replace it with the scrollButton slot.

The field is ChatComposer, placed in the layout's composer slot. The shell owns the draft, send, and stop. The default editor accepts chips, sends on Enter, and inserts a newline on Shift+Enter. The attachment drawer, header, and footer are slots. Dictation uses the browser's speech recognition, and the microphone stays hidden when that API is missing. The landing page and the transcript with a document panel are in Patterns.

vue
<script setup lang="ts">
import { ChatComposer, ChatLayout, ChatMessage, ChatMessageBubble, ChatMessageList } from '@astryx-vue/core/Chat'

function onSubmit(value: string) {
  console.log(value)
}
</script>

<template>
  <ChatLayout>
    <ChatMessageList>
      <ChatMessage sender="user">
        <ChatMessageBubble>
          Check the release notes for me.
        </ChatMessageBubble>
      </ChatMessage>
      <ChatMessage sender="assistant">
        <ChatMessageBubble name="Assistant">
          The notes are in the docs.
        </ChatMessageBubble>
      </ChatMessage>
    </ChatMessageList>
    <template #composer>
      <ChatComposer placeholder="Type a message…" @submit="onSubmit" />
    </template>
  </ChatLayout>
</template>

ChatMessageList is a role="log". Pass isStreaming while an assistant reply is still growing, so a screen reader waits and announces the finished message once. scrollToTopAction runs when the top sentinel enters the scroll parent, and a spinner stays up until the promise settles.

ChatMessage is an article. sender is user, assistant, or system. A system row is centered and ignores the avatar, name, and metadata slots. Put the visible name on ChatMessageBubble when the body starts with a bubble, so the name lines up with the bubble padding.

ChatToolCalls takes the tool list an assistant turn already produced. One call is a single row. Several calls collapse behind a header; label replaces the localized count while the group is open. A call with detail, or the detail slot, can expand on its own. v-model:is-expanded controls the group.

ChatReasoning is the same kind of disclosure for a thinking trace. It starts collapsed. A plain-text default slot is reused as the one-line preview. isStreaming shimmers the label and hides both the preview and duration.

ChatTypingIndicator draws three dots. Pass names and it writes a sentence for one person, two people, or a named person plus a count. The root is role="status", and that region is the only announcement. ChatUnreadDivider marks the first unread message, and new rows go below it.

The examples start with the composer beside the transcript, then follow layout, scroll button, message, bubble, list, metadata, system message, and tool calls.

Anatomy

ElementDescription
Message areaScrollable region for messages. Renders the default slot (typically ChatMessageList) in a flex column that pushes content to the bottom when the list is short.
Frosted glass dockSticky or fixed container at the bottom with a backdrop-blur layer. Houses the scroll button and composer.
Scroll-to-bottom buttonAppears when the user scrolls up or new messages arrive. Defaults to ChatLayoutScrollButton. Set showScrollButton to false to hide it, or replace it with the scrollButton slot.
ComposerThe input area for sending messages, typically ChatComposer. Docked at the bottom inside the frosted glass layer.
Empty stateCentered placeholder shown when no messages exist. Use EmptyState for a consistent look.
AvatarA sender avatar rendered beside the message. Typically Avatar with size="md". Hidden for system messages.
NameSender name above the message body. Place on the bubble when using bubbles, or on the message wrapper for raw content.
ContentThe message body: one or more ChatMessageBubble elements, or any free-form slot content like images or tool calls. Wrap non-bubble content in a ghost bubble to align it with the bubble text column.
MetadataTimestamp, delivery status, and footer actions below the message. Place on the last bubble or on the message wrapper.

Best practices ​

GuidancePractices
DoKeep the transcript in the message list and the composer in the bottom slot, so the frosted dock and the scroll button stay aligned.
DoSet isStreaming for the whole time an assistant body is growing, and clear it when the message is complete.
DoUse ChatUnreadDivider at the first message the reader has not seen. The scroll button’s “New messages” label is the announcement; the divider is the visible mark.
DoPut plain text, citations, or your own renderer in the bubble slot. The bubble only supplies the surface.
Don'tRender a second scroll parent inside the layout. One element should own the transcript scroll, either the layout or the element passed as scrollRef.
Don'tAnnounce every streamed token yourself. The typing indicator speaks once through its own role="status", and the new-message label speaks through the layout live region.

Examples ​

ChatLayout — Panel View

Can you review the Button component and fix the focus ring?

Found the issue — the border radius was hardcoded. Replaced with the theme token.

A narrow column. Press Enter and the draft is appended to the transcript. A tight container uses compact density, which fits a side panel or a drawer.

ChatComposer — Shell

notes.mdfocus.png
Balanced

A field with an attachment drawer, a header button, and dictation. With no text the send button is muted and cannot be clicked; it turns into the solid button once there is something to send. Attachments alone do not enable it. The drawer handle collapses the attachments. The microphone starts listening in browsers that support speech recognition.

ChatComposer — States

The connection is slow.

The top field is disabled while a reply is in progress, and Stop stays clickable. The middle one is compact and flat, with a warning. The bottom one puts an error above the field.

ChatComposerInput — Mentions

Type @ at the start of a word and keep going through spaces, as in Ada Lov, and the list narrows to Ada Lovelace. Arrow keys move the highlight, and Enter inserts that row rather than the first person in the list. A phrase with no match hides the list and leaves the typed text alone. Type / for a one-word command; a space closes that list. A person becomes a chip. A command is inserted as text.

ChatTokenizedText — Chips in a bubble

Ask @Ada to check the focus ring.

The same chip definitions render inside a sent bubble.

ChatLayoutScrollButton — Labels

Labels expand the button to give context

Scroll button with different labels for context-specific notifications like new messages, unread replies, or a generic scroll prompt.

ChatMessage — Avatar & Name

A
Agent
I reviewed the pull request. The changes look solid — clean code and good test coverage.
Thanks! Merging it now.
A
Agent
I can run the deployment pipeline once it lands. Just let me know.

Messages with avatars and sender names. Place the name on the bubble when using bubbles, or on the message wrapper for raw content.

ChatMessage — Ghost

Here is an analysis of your production metrics from last week. Traffic peaked at 12,400 requests per second on Wednesday, with a p99 latency of 45ms. Error rate stayed below 0.1% across all endpoints.
That looks great. Can you compare it to the week before?
Compared to the previous week, traffic is up 8% and latency improved by 3ms. The deployment on Tuesday seems to have helped.

Ghost variant for messages without visible bubble boundaries. Keeps padding for alignment but renders a transparent background, useful for AI-style responses.

ChatMessage — Multi-Bubble

I have a couple of questions about the new API.
First, how should we handle pagination?
And second, what's the rate limit?
A
Agent
Great questions! For pagination, use cursor-based with a limit parameter. The response includes a nextCursor field.
Rate limit is 100 requests per minute per API key. You'll get a 429 response with a Retry-After header if you exceed it.

Grouped bubbles using the group prop for corner radius reduction. Use first, middle, and last to visually connect related bubbles from the same sender.

ChatMessageBubble — Custom Content

Can you pull up the Q3 report?
Navi
Sure, here is the artifact from last quarter.

Q3 Performance Report

12 pages · updated 2 days ago

Custom in-message content aligned to the bubble text column. A ghost bubble spans the message column, and the timestamp rides the bubble metadata slot.

ChatMessageBubble — Density

Compact

The build completed in 4.2 seconds.
Ship it to staging.

Balanced

The build completed in 4.2 seconds.
Ship it to staging.

Spacious

The build completed in 4.2 seconds.
Ship it to staging.

Compact, balanced, and spacious density modes side by side. Density controls bubble padding, corner radius, and spacing between grouped bubbles.

ChatMessageBubble — Grouping

A
Agent
I reviewed the three files you shared.
The data model looks solid, but the API handler has a race condition on concurrent writes.
I can draft a fix if you want.
Yes please!
Also add a test for the concurrent case.

Multi-bubble messages using first, middle, and last group positions. Grouped bubbles tighten corner radius on the sender side.

ChatMessageBubble — Metadata

A
Agent
Your deployment finished successfully. All 14 checks passed.
Great, can you send me the production URL?

Bubbles with name and metadata slots aligned to bubble padding. Put name on the first bubble and metadata on the last bubble in a message.

ChatMessageBubble — Variants

Filled — sender-colored background (default)

Can you summarize the latest deployment logs?

Ghost — transparent background, keeps alignment padding

The last deploy completed at 2:41 PM with zero errors across all three regions.

Filled and ghost bubble variants for both user and assistant senders. Use filled for standard messages and ghost when content needs alignment without a visual boundary.

ChatMessageList — Density

Compact

How does density work?
A
Density provides default spacing at every level — message gap, bubble padding, and gap between child elements. Use gap to tune row spacing independently.

Balanced

How does density work?
A
Density provides default spacing at every level — message gap, bubble padding, and gap between child elements. Use gap to tune row spacing independently.

Spacious

How does density work?
A
Density provides default spacing at every level — message gap, bubble padding, and gap between child elements. Use gap to tune row spacing independently.

Side-by-side comparison of compact, balanced, and spacious densities. Use compact in sidebars, balanced for most full-page chat, and spacious for long-form reading.

ChatMessageList — Full Featured

useReducer.tsformState.ts
Can you review these files?
A
Sure! Here's the key pattern from useReducer.ts:
The reducer is pure and easy to test — pass in state and action, assert on the output.
    
tsx
const [state, dispatch] = useReducer(
(state, action) => ({
...state,
[action.field]: action.value,
}),
{ name: '', email: '' }
);
Agent shared a code snippet
That's clean, thanks!

A transcript with a date divider, file names, a code sample, ghost bubbles, and metadata.

ChatMessageMetadata — Footer Actions

Summarize the Q1 revenue report.
Q1 revenue reached $2.4B, up 18% year-over-year. Enterprise subscriptions drove 62% of the growth, while ad revenue held steady at $890M.

Assistant message with footer actions: copy, retry, thumbs up/down, and model label.

ChatMessageMetadata — Status

Deploying the update now…
Config pushed to staging.
Verified on the staging cluster.
Looks good — promoting to prod.
Rollback triggered, checking logs.

All 5 delivery statuses (sending, sent, delivered, read, and error), each with a timestamp.

ChatMessageMetadata — Timestamps

Thanks — any blockers I should know about?
Relative timestamps work too — helpful for older messages where the exact time matters less than recency.

Timestamp-only metadata on user and assistant messages. Supports an absolute time and a relative label.

ChatSystemMessage — Icon

Icons reinforce the message type

Jordan was added to the conversation
Messages are end-to-end encrypted
Agent is generating a response…
Conversation verified by admin

System messages with a leading icon that reinforces the message type.

ChatSystemMessage — Status Updates

Sarah Chen joined the conversation
Topic changed to "Q2 Launch Planning"
Alex Rivera left the conversation
Conversation marked as resolved

Status messages in a conversation flow: membership changes, a topic change, dates, and a resolution notice.

ChatSystemMessage — Variants

Default

Alex joined the conversation
Conversation marked as resolved

Divider

Default and divider variants side by side. Use default for inline status updates and divider for date separators.

ChatToolCalls — Expandable

Tool calls with expandable result details. Click a row to reveal its result.

ChatToolCalls — Simple

Sentbashgit status1.2s
Sentreadsrc/components/DataGrid.tsx30ms
Senteditsrc/components/DataGrid.tsx+24-885ms
Senteditsrc/components/DataGrid.test.tsx+4560ms

A single inline tool call above a collapsible multi-call group with diff stats.

ChatToolCalls — Statuses

bashyarn build
bashyarn test
Senteditsrc/App.tsx+8-2120ms
Error: 3 lint errors foundbashyarn lint

All four status states (pending, running, complete, and error) shown together in a single group.