Conversation
An AI chat transcript: role-attributed messages in a scrollable column that follows new replies without ever yanking a reader who scrolled up. Built as the assist panel for AI products.
<arc-conversation> Overview
Guidelines
When to use
- Use Conversation for a dialogue between the user and a responder — the role attribution and scroll behavior are the point
- Stream a reply by appending to the message's slotted text (for example, updating textContent as tokens arrive); with markdown set it re-renders as it grows
- Show a pending assistant message the moment a request is sent, then fill its slot when tokens arrive
- Give messages timestamps as ISO strings and let the house relative-time rendering do the rest
- Listen for arc-scroll-away to float a "jump to latest" chip, and clear it on arc-scroll-return
- Size the transcript with the --conversation-height custom property, or let it fill a sized parent
When not to use
- Do not use Conversation for an activity feed or event history — that is Timeline's job, where entries mark moments rather than speakers
- Do not use it as a general item column — a List handles collections that no one is talking to
- Do not scroll the reader to the bottom yourself on new messages — the component already follows, and only when the reader wants it to
- Do not put critical status in a system message alone; it scrolls away with the transcript
- Do not build avatars into every product by reflex — the avatar slot is there for the products that need one
Features
- Three message voices: user on an accent tint at the inline end, assistant on a neutral surface, system centered and muted
- Auto-scroll follows new and growing messages — only while the reader is already near the bottom
- `arc-scroll-away` / `arc-scroll-return` events with the distance from the bottom, for a "jump to latest" chip
- `scrollToEnd()` method to jump the transcript to its newest message
- Streaming-friendly: append text to a message slot and the view keeps up
- Markdown rendering of message bodies through `arc-markdown`, re-parsed as streamed text grows
- Typing indicator on `pending` messages, replaced by a static ellipsis under `prefers-reduced-motion`
- Relative timestamps through `arc-time-ago`, with the absolute date on hover
- An `avatar` slot on each message for products that want faces — none are built in
- RTL mirrors automatically: alignment is logical-properties only
- Server-renders in full, pending dots included
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.
<arc-conversation style="--conversation-height: 400px">
<arc-message speaker="user" author="You" timestamp="2026-07-31T14:02:00Z">
How do I loop the chorus while I try out lead takes?
</arc-message>
<arc-message speaker="assistant" author="daw[n]" markdown timestamp="2026-07-31T14:02:04Z">
Drag the **loop brace** over bars 17-24, press L, then arm the
lead track — every pass lands on its own take lane.
</arc-message>
<arc-message speaker="assistant" author="daw[n]" pending></arc-message>
</arc-conversation>
<script>
// Streaming: fill the pending message as tokens arrive.
const reply = document.querySelector('arc-message[pending]');
function onToken(token) {
reply.pending = false;
reply.markdown = true;
reply.textContent += token;
}
</script> import { Conversation, Message } from '@arclux/arc-ui-react';
export default function AssistPanel({ messages, streaming }) {
return (
<Conversation style={{ '--conversation-height': '400px' }}>
{messages.map((m) => (
<Message key={m.id} speaker={m.role} author={m.author} timestamp={m.at} markdown>
{m.text}
</Message>
))}
{streaming && <Message speaker="assistant" author="daw[n]" pending />}
</Conversation>
);
} <script setup>
import { Conversation, Message } from '@arclux/arc-ui-vue';
defineProps(['messages', 'streaming']);
</script>
<template>
<Conversation style="--conversation-height: 400px">
<Message
v-for="m in messages"
:key="m.id"
:speaker="m.role"
:author="m.author"
:timestamp="m.at"
markdown
>{{ m.text }}</Message>
<Message v-if="streaming" speaker="assistant" author="daw[n]" pending />
</Conversation>
</template> <script>
import { Conversation, Message } from '@arclux/arc-ui-svelte';
export let messages = [];
export let streaming = false;
</script>
<Conversation style="--conversation-height: 400px">
{#each messages as m (m.id)}
<Message speaker={m.role} author={m.author} timestamp={m.at} markdown>{m.text}</Message>
{/each}
{#if streaming}
<Message speaker="assistant" author="daw[n]" pending />
{/if}
</Conversation> import { Component, Input } from '@angular/core';
import { Conversation, Message } from '@arclux/arc-ui-angular';
@Component({
imports: [Conversation, Message],
template: `
<arc-conversation style="--conversation-height: 400px">
@for (m of messages; track m.id) {
<arc-message [attr.speaker]="m.role" [attr.author]="m.author" [attr.timestamp]="m.at" markdown>
{{ m.text }}
</arc-message>
}
@if (streaming) {
<arc-message speaker="assistant" author="daw[n]" pending></arc-message>
}
</arc-conversation>
`,
})
export class AssistPanelComponent {
@Input() messages: Array<{ id: string; role: string; author: string; at: string; text: string }> = [];
@Input() streaming = false;
} import { Conversation, Message } from '@arclux/arc-ui-solid';
import { For, Show } from 'solid-js';
export default function AssistPanel(props) {
return (
<Conversation style={{ '--conversation-height': '400px' }}>
<For each={props.messages}>
{(m) => (
<Message speaker={m.role} author={m.author} timestamp={m.at} markdown>
{m.text}
</Message>
)}
</For>
<Show when={props.streaming}>
<Message speaker="assistant" author="daw[n]" pending />
</Show>
</Conversation>
);
} import { Conversation, Message } from '@arclux/arc-ui-preact';
export default function AssistPanel({ messages, streaming }) {
return (
<Conversation style={{ '--conversation-height': '400px' }}>
{messages.map((m) => (
<Message key={m.id} speaker={m.role} author={m.author} timestamp={m.at} markdown>
{m.text}
</Message>
))}
{streaming && <Message speaker="assistant" author="daw[n]" pending />}
</Conversation>
);
} API
-
auto-scrollbooleantrue - Follow new content: when a message is added or grows while the reader is near the bottom, scroll to keep the latest visible. A reader who has scrolled up is never pulled back down. Defaults to true; set the property to false to leave scrolling entirely to the consumer.
Events
-
arc-scroll-awaydetail: {value: number} - Fired once when the reader scrolls up out of the near-bottom zone of the transcript. detail.value is the distance from the bottom in pixels.
-
arc-scroll-returndetail: {value: number} - Fired once when the reader comes back within the near-bottom zone. detail.value is the distance from the bottom in pixels.
Message
<arc-message> One message in the transcript. The speaker attribute picks the voice (user, assistant, or system), author and timestamp fill the muted meta line, markdown renders the slotted text through the house renderer, and pending shows the typing indicator until a reply arrives.
-
speaker'user' | 'assistant' | 'system''user' - Whose message this is. "user" aligns to the inline end on an accent-tinted surface, "assistant" to the inline start on a neutral surface, and "system" runs centered and muted for notices in the transcript's own voice. An unrecognized value renders as "user".
-
authorstring'' - Display name shown in the muted meta line above the bubble. Omit it and the meta line only appears when a timestamp is set.
-
timestampstring'' - When the message was sent, as an ISO 8601 string. Rendered as house relative time ("3 minutes ago") through arc-time-ago, with the absolute date on its title.
-
pendingbooleanfalse - Renders the typing indicator — three pulsing dots — in place of the body while a reply is being produced. Under prefers-reduced-motion the dots give way to a static ellipsis.
-
markdownbooleanfalse - Render the slotted text through the house markdown renderer. The slot's text content is the source; it re-parses whenever the slot changes, so streaming into the slot streams through the renderer. When false, slotted content renders as-is.
See Also
- Markdown Renders markdown content as styled HTML with zero dependencies. Supports headings, lists, code blocks, blockquotes, links, images, and inline formatting.
- Timeline Vertical timeline with dated events.
- List Structured list container with optional selection, keyboard navigation, and multiple visual variants. Pairs with arc-list-item for rich content rows.
- Time Ago Relative time display that auto-updates ("3 minutes ago", "yesterday").