Getting StartedComponentsDesign TokensThemingTheme SynthesizerFrameworksAccessibilityUtilitiesServer RenderingBrowser SupportContributingChangelog App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip
ARC UI ARC Radiant Components
v3.2 Docs Components Tokens Synthesizer
Getting StartedFrameworksServer Rendering Design TokensThemingTheme SynthesizerTypographyUtilities All ComponentsAccessibilityBrowser SupportChangelogContributingStats App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip

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.

Components Conversation
feedback interactive
<arc-conversation>

Overview

Conversation is the transcript surface for an AI assistant panel. Slot `<arc-message>` children into it and each one takes a voice from its speaker attribute: user messages align to the inline end on a faint accent tint, assistant messages answer from the inline start on a neutral surface, and system messages run centered and muted for notices in the transcript's own voice. Because alignment is built on logical properties, the whole layout mirrors automatically in RTL. The container is the scroll area, and it knows the one rule every chat interface lives by: while the reader is near the bottom, new or growing messages keep the view pinned to the latest; the moment they scroll up to re-read, the transcript stays put. The `arc-scroll-away` and `arc-scroll-return` events report those transitions with the distance from the bottom, so a consumer can float a "jump to latest" chip and wire it to the `scrollToEnd()` method. Streaming needs no API of its own — a message body is its default slot, so appending text to the slot streams the reply in. With the `markdown` attribute set, the slotted text renders through the house markdown renderer and re-parses as it grows, and a `pending` message shows the typing indicator until the first tokens arrive. The transcript server-renders in full, typing dots included.

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

Project "Neon Tides" opened — the assistant can see your arrangement. How do I loop the chorus while I try out lead takes? Drag the **loop brace** over bars 17-24, then: 1. Press L to switch looping on 2. Arm the lead track 3. Hit record — every pass lands on its own take lane Can it keep every take, or just the last one?

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-scroll boolean true
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-away detail: {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-return detail: {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".
author string ''
Display name shown in the muted meta line above the bubble. Omit it and the meta line only appears when a timestamp is set.
timestamp string ''
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.
pending boolean false
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.
markdown boolean false
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