AI Interface Components

Scripted Chat Thread

A scripted chat thread React component: user bubbles spring in, a dotted typing indicator bounces, and assistant replies stream character by character, including code blocks, with optional looping.

Turns a written conversation script into a chat screen that plays itself — handy for landing pages, feature tours or walking through a support flow. User messages spring in from the bottom right with a slight overshoot; before each assistant reply a three-dot typing indicator appears, then the bubble springs in from the left and the text streams character by character, while an attached code block appears line by line with line numbers and a copy button. Playback starts once the thread enters the viewport and auto-scrolls to the bottom as content arrives; turn on loop to fade out, clear and start over after each run. Optionally wrap it in a simplified CSS phone shell with frame="phone".

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add scripted-chat-thread.en

New project or installation trouble? Start here →

Playground

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

22
400
Active edited usageDefault props
<ScriptedChatThread/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/scripted-chat-thread.en.json

Or, once registries are configured in components.json, install it as @webberui/scripted-chat-thread.

Usage

import { ScriptedChatThread } from "@/components/ui/scripted-chat-thread";

<ScriptedChatThread
  frame="phone"
  loop
  messages={[
    { role: "user", content: "My order was placed three days ago and still hasn't shipped" },
    { role: "assistant", content: "Sorry for the wait! Could you share your order number?" },
    { role: "user", content: "#BK20261025" },
    {
      role: "assistant",
      typingMs: 1200,
      content: "It has been picked and should ship tomorrow. Lookup example:",
      code: {
        lang: "ts",
        text: 'const res = await fetch("https://api.example.com/v1/orders/BK20261025");',
      },
    },
  ]}
  onComplete={() => console.log("finished")}
/>

To control playback yourself, pass a ref to get restart():

const threadRef = React.useRef<ScriptedChatThreadHandle>(null);

<ScriptedChatThread ref={threadRef} autoplay={false} messages={script} />
<button onClick={() => threadRef.current?.restart()}>Replay</button>

Props

PropTypeDefaultDescription
messagesScriptedChatMessage[]—The conversation script, played in order
autoplaybooleantrueAutoplay; when true playback starts once in view, when false a play button shows (or call ref.restart())
loopbooleanfalseAfter finishing, hold 2500ms, fade out, clear and replay
charMsnumber22Milliseconds per assistant character; when omitted reads the CSS variable --wb-sct-char-ms, then falls back to 22
gapMsnumber600Gap in milliseconds between messages
springConfigScriptedChatSpring{ stiffness: 400, damping: 28 }Spring parameters for bubble entrances
frame"none" | "phone""none"Frame: none is a plain card, phone wraps the thread in a simplified CSS phone shell
heightnumber | string440Height of the scrolling thread (numbers are px)
avatarReact.ReactNodeCircle with the letter "A"Assistant avatar; defaults to an accent-colored letter circle
ariaLabelstring"Conversation demo"Accessible name of the container
onComplete() => void—Called when the whole script has played (once per round with loop)
refReact.Ref<ScriptedChatThreadHandle>—Imperative API exposing restart()
classNamestring—Passed through to the outermost container

ScriptedChatMessage

FieldTypeDefaultDescription
idstringindexMessage id used as the React key
role"user" | "assistant"—Speaker: user sits on the right, assistant on the left with an avatar
contentstring—Text content; assistant replies stream character by character
codeScriptedChatCode—Code block; streams line by line after the text
typingMsnumber900How long the typing indicator shows before an assistant reply

ScriptedChatCode

FieldTypeDefaultDescription
langstring—Language tag, shown at the top-left of the block
textstring—Source text, split on newlines

ScriptedChatSpring

FieldTypeDefaultDescription
stiffnessnumber400Stiffness — higher is faster and bouncier
dampingnumber28Damping — lower gives more overshoot

ScriptedChatThreadHandle

FieldTypeDefaultDescription
restart() => void—Clear the thread and replay from the first message

How it works

  • Pacing: a user bubble springs in, then holds for 400ms; an assistant reply first shows the typing indicator for typingMs, then streams its text (charMs per character) and its code line by line (charMs × 8 per line); messages are separated by gapMs.
  • Auto-scroll: the thread only follows new content when the reader is already at the bottom (within 24px); scrolling up pauses following and shows a small "Jump to latest" button that returns to the bottom and resumes.
  • messages is compared by content, so an inline array does not restart playback when the parent re-renders.
  • CSS variables: --wb-sct-char-ms sets the default streaming speed (the charMs prop wins), and --wb-sct-accent sets the accent color of the avatar and cursor.
  • You can nest this component inside chat-scroller, but neither depends on the other; this component already has its own scroll-to-bottom and "Jump to latest" behavior.

Accessibility

  • The scroll container is role="log" with aria-live="polite", named by ariaLabel (default "Conversation demo")
  • Partial streaming text is hidden from assistive tech; only once a message completes is its full text (with the speaker) placed in a readable node, so screen readers do not read it out character by character
  • The typing indicator carries an sr-only "Typing" label; the code copy button, "Jump to latest" and "Play conversation" are all type="button" with a visible focus ring
  • When the system "reduce motion" setting is on, there is no spring-in, no streaming and no typing indicator — every message appears at once with a 150ms fade, and onComplete fires immediately