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.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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.jsonOr, 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
| Prop | Type | Default | Description |
|---|---|---|---|
messages | ScriptedChatMessage[] | — | The conversation script, played in order |
autoplay | boolean | true | Autoplay; when true playback starts once in view, when false a play button shows (or call ref.restart()) |
loop | boolean | false | After finishing, hold 2500ms, fade out, clear and replay |
charMs | number | 22 | Milliseconds per assistant character; when omitted reads the CSS variable --wb-sct-char-ms, then falls back to 22 |
gapMs | number | 600 | Gap in milliseconds between messages |
springConfig | ScriptedChatSpring | { 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 |
height | number | string | 440 | Height of the scrolling thread (numbers are px) |
avatar | React.ReactNode | Circle with the letter "A" | Assistant avatar; defaults to an accent-colored letter circle |
ariaLabel | string | "Conversation demo" | Accessible name of the container |
onComplete | () => void | — | Called when the whole script has played (once per round with loop) |
ref | React.Ref<ScriptedChatThreadHandle> | — | Imperative API exposing restart() |
className | string | — | Passed through to the outermost container |
ScriptedChatMessage
| Field | Type | Default | Description |
|---|---|---|---|
id | string | index | Message id used as the React key |
role | "user" | "assistant" | — | Speaker: user sits on the right, assistant on the left with an avatar |
content | string | — | Text content; assistant replies stream character by character |
code | ScriptedChatCode | — | Code block; streams line by line after the text |
typingMs | number | 900 | How long the typing indicator shows before an assistant reply |
ScriptedChatCode
| Field | Type | Default | Description |
|---|---|---|---|
lang | string | — | Language tag, shown at the top-left of the block |
text | string | — | Source text, split on newlines |
ScriptedChatSpring
| Field | Type | Default | Description |
|---|---|---|---|
stiffness | number | 400 | Stiffness — higher is faster and bouncier |
damping | number | 28 | Damping — lower gives more overshoot |
ScriptedChatThreadHandle
| Field | Type | Default | Description |
|---|---|---|---|
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 (charMsper character) and its code line by line (charMs× 8 per line); messages are separated bygapMs. - 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.
messagesis compared by content, so an inline array does not restart playback when the parent re-renders.- CSS variables:
--wb-sct-char-mssets the default streaming speed (thecharMsprop wins), and--wb-sct-accentsets 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"witharia-live="polite", named byariaLabel(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
onCompletefires immediately
Chat Scroller
A scroll container for chat: it follows streaming output while pinned to the bottom, shows a new-messages pill when you scroll away, keeps position when loading history, and can scroll to any message on demand.
Code Change Explainer
A stepped diff walkthrough: each step highlights removed or added lines while a side note card advances in sync, explaining the change in plain language.