AI Interface Components

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.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.

55
0
2
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
textstring | string[]—Sentence to type in sync; pass an array to cycle through several
cellsSyncedTypingCell[]—Per-cell interface settings (2–6 cells recommended)
typeSpeednumber55Delay between characters (ms)
startDelaynumber300Wait before each sentence starts typing (ms)
pauseDurationnumber1800How long to hold after typing before the next sentence or retype (ms)
loopbooleantrueWhether to loop; when false it stops after the last sentence
jitternumber0Max deterministic per-cell time offset (± ms) for an "almost in sync" stagger; 0 is perfectly in sync
columnsnumber2Column count on large screens (lg and up), 1–6; phones always use 1 column, sm uses 2
playingboolean—Controlled playing state; managed internally when omitted
defaultPlayingbooleantrueInitial 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
showControlsbooleantrueWhether to show the play/pause button
labelstring"Synced typing showcase"Accessible name of the root element
pauseLabelstring"Pause"Pause button text
playLabelstring"Play"Play button text
replayLabelstring"Replay"Replay button text shown after playback ends
classNamestring—Passed through to the outermost container

SyncedTypingCell

FieldTypeDefaultDescription
idstring—Unique id, used as the React key
labelstring—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
resultReactNode—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 jitter above 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 loop set to false playback 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, default 1s) and --wb-synced-typing-result-duration (result fade-in, default 320ms); the send button and status dot color transitions use --wb-duration-fast and --wb-ease-out.

Accessibility

  • The root is a role="group" named by label
  • The letter-by-letter typing areas are aria-hidden; a separate sr-only copy of the full sentence is read instead, so screen readers never announce letter by letter
  • The play/pause control is a native button whose 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