WebberUI

AI Message Stream

A streaming message list — character-by-character typing fade-in, Markdown surfacing progressively, with built-in stop / retry animations and auto-scroll.

This is a WebberUI Pro component

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

Loading preview…
npx shadcn@latest add "https://webberui.com/r/ai-message-stream.json?t=<install token>"

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

45
320
<AiMessageStream />

Installation

npx shadcn@latest add "https://webberui.com/r/ai-message-stream.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/ai-message-stream.

Usage

import {
  AIMessageStream,
  type StreamMessage,
} from "@/components/ui/ai-message-stream";

const messages: StreamMessage[] = [
  { id: "u1", role: "user", content: "Tell me about streaming replies", status: "done" },
  { id: "a1", role: "assistant", content: "This is a **streaming** reply…", status: "streaming" },
];

<AIMessageStream
  messages={messages}
  streaming={isStreaming}
  onStop={() => controller.abort()}
  onRetry={regenerate}
/>

Just bind streaming to the model's streaming state: while true, the last assistant message surfaces character by character at charsPerSecond and a "Stop generating" button appears in the header row; once it flips back to false, "Regenerate" is shown instead. Assistant message content surfaces as lightweight Markdown — bold, italics, inline code, lists, headings, and links take shape the moment their syntax completes.

With a real stream, just keep merging newly received tokens into that message's content and the reveal animation catches up to the current length automatically; giving content all at once (as in the example above) types out the whole thing.

Props

AIMessageStream

PropTypeDefaultDescription
messagesStreamMessage[]Array of messages
streamingbooleanfalseWhen true, the last assistant message is revealed character by character
onStop() => voidCallback for clicking "Stop"; providing it shows the stop button while streaming
onRetry() => voidCallback for clicking "Retry"; providing it shows the retry button while not streaming
charsPerSecondnumber45Typing reveal speed (characters / second)
markdownbooleantrueWhether assistant messages are parsed as Markdown
autoScrollbooleantrueScrolls to the bottom automatically as content grows (paused while manually scrolled up)
maxHeightnumber | string320Maximum height of the scroll container
classNamestringAppended to the outer container's className

StreamMessage

FieldTypeDescription
idstringUnique identifier, used as the key and as the boundary for the character-by-character reveal
role"user" | "assistant"Role: the user is right-aligned, the assistant left-aligned
contentstringThe content; assistant messages surface as Markdown by default
status"streaming" | "done" | "stopped" | "error"Status; error / stopped show the matching notice

How it works

  • The character-by-character reveal advances via requestAnimationFrame at charsPerSecond, catching up to the current length automatically as the content grows
  • Each word surfaces on mount with a fade in, an upward shift, and a de-blur, and Markdown blocks take shape as their syntax closes
  • An unclosed code block (mid-stream) still renders live as code, matching how a real stream looks
  • A blinking cursor is shown while streaming, and a three-dot waiting indicator appears while there is no content yet
  • "Stop / Retry" swap in and out with AnimatePresence, and the stop button carries a pulse
  • Content growth auto-scrolls while pinned to the bottom, pauses when the user scrolls up, and resumes on returning to the bottom
  • Every requestAnimationFrame and timer is cleaned up on state changes and on unmount

Accessibility

  • The scroll container carries role="log" and aria-live="polite", so streamed content is announced by assistive technology
  • Message bubbles carry aria-busy while streaming, which is cleared once done
  • Links carry rel="noopener noreferrer" and open in a new tab
  • When the user has "reduce motion" enabled at the system level: the character-by-character reveal is skipped in favor of showing the complete content, the cursor and per-word animations are removed, and the waiting indicator and stop pulse degrade to static

On this page