Synced Typing Grid
A synced typing showcase React component that types the same prompt simultaneously into N differently styled input shells — chat, terminal, search bar and document editor — for side-by-side interface comparisons.
Types the same prompt into 2–6 differently styled input boxes at once: every cell shares one typing clock, so characters appear in sync. Once the sentence is done, each cell can fade in a mock result before clearing and retyping, or moving on to the next sentence. Four shells are built in — chat, terminal, search and editor — and render lets you pass your own. Use it on a landing page to show that one feature works in chat, the command line, search and the editor alike, or to compare how several interfaces format a response side by side.
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 synced-typing-grid.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<SyncedTypingGrid/>
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/synced-typing-grid.en.jsonOr, once registries are configured in components.json, install it as @webberui/synced-typing-grid.
Usage
import { SyncedTypingGrid } from "@/components/ui/synced-typing-grid";
<SyncedTypingGrid
text="Summarize last week's customer complaints into three key points"
columns={2}
jitter={40}
cells={[
{ id: "chat", label: "Chat assistant", variant: "chat", result: <p>Summarized into three points</p> },
{ id: "terminal", label: "Command line", variant: "terminal" },
{ id: "search", label: "Site search", variant: "search" },
{
id: "custom",
label: "Custom shell",
render: (typed, done) => (
<div className="rounded-lg border p-3 text-sm">
{typed}
{done ? " ✓" : null}
</div>
),
},
]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | string[] | — | Sentence to type in sync; pass an array to cycle through several |
cells | SyncedTypingCell[] | — | Per-cell interface settings (2–6 cells recommended) |
typeSpeed | number | 55 | Delay between characters (ms) |
startDelay | number | 300 | Wait before each sentence starts typing (ms) |
pauseDuration | number | 1800 | How long to hold after typing before the next sentence or retype (ms) |
loop | boolean | true | Whether to loop; when false it stops after the last sentence |
jitter | number | 0 | Max deterministic per-cell time offset (± ms) for an "almost in sync" stagger; 0 is perfectly in sync |
columns | number | 2 | Column count on large screens (lg and up), 1–6; phones always use 1 column, sm uses 2 |
playing | boolean | — | Controlled playing state; managed internally when omitted |
defaultPlaying | boolean | true | Initial playing state in uncontrolled mode |
onPlayingChange | (playing: boolean) => void | — | Called when the playing state changes |
onDone | (sentenceIndex: number) => void | — | Called when every cell has finished a sentence; receives the sentence index |
showControls | boolean | true | Whether to show the play/pause button |
label | string | "Synced typing showcase" | Accessible name of the root element |
pauseLabel | string | "Pause" | Pause button text |
playLabel | string | "Play" | Play button text |
replayLabel | string | "Replay" | Replay button text shown after playback ends |
className | string | — | Passed through to the outermost container |
SyncedTypingCell
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique id, used as the React key |
label | string | — | Interface name shown above the cell |
variant | "chat" | "terminal" | "search" | "editor" | "chat" | Built-in shell style; ignored when render is provided |
render | (typed: string, done: boolean) => ReactNode | — | Custom shell: receives the text typed so far and whether typing is done, returns any node |
result | ReactNode | — | Node that fades in once the sentence is fully typed (e.g. a mock reply) |
How it works
- One shared clock: a single timer advances a shared elapsed time, and every cell derives how many characters to show from it, so all cells type in lockstep. With
jitterabove 0, each cell gets a fixed offset from a hash of its index (no random numbers), so the server and the browser compute the same value and every retype keeps the same rhythm. - Text is split with
Array.from, by code point, so CJK characters and emoji never break apart. - Arrays cycle in order; with
loopset tofalseplayback stops after the last sentence and the button turns into Replay. - Pausing and resuming continues from where it left off rather than starting over.
- Timing is overridable through CSS variables:
--wb-synced-typing-caret(caret blink period, default1s) and--wb-synced-typing-result-duration(result fade-in, default320ms); the send button and status dot color transitions use--wb-duration-fastand--wb-ease-out.
Accessibility
- The root is a
role="group"named bylabel - The letter-by-letter typing areas are
aria-hidden; a separatesr-onlycopy of the full sentence is read instead, so screen readers never announce letter by letter - The play/pause control is a native
buttonwhose text follows the state (Pause / Play / Replay), is keyboard operable and has a focus-visible ring - The component pauses automatically while off-screen (IntersectionObserver) and resumes when scrolled back into view
- When the user has Reduce Motion turned on, the full sentence and results are shown immediately — no typing, no caret blink and no play button
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.
Generate-and-Pick Flow
Prompt, skeletons, then a row of generated results slides in for the user to pick one that expands for confirmation — the standard flow for image and video generators.