Skip to content

fix(animate): serialize stagger delays across sibling blocks to prevent concurrent animation#493

Open
sleitor wants to merge 1 commit intovercel:mainfrom
sleitor:fix/animate-serial-blocks-#482
Open

fix(animate): serialize stagger delays across sibling blocks to prevent concurrent animation#493
sleitor wants to merge 1 commit intovercel:mainfrom
sleitor:fix/animate-serial-blocks-#482

Conversation

@sleitor
Copy link
Copy Markdown
Contributor

@sleitor sleitor commented Apr 2, 2026

Summary

Fixes #482

When streaming markdown with multiple blocks, animations across sibling blocks revealed concurrently — a list could still be animating while the next paragraph was already fading in.

Root Cause

All blocks shared a single AnimatePlugin instance with a fixed startIndex=0. When a new block appeared, its first word started animating at delay=0ms, regardless of what was animating in the previous block.

Additionally, lastRenderNewWordCount was never written back to renderState in the rehype closure.

Fix

Introduces AnimateCursor — a lightweight shared counter ({ current: number }) that:

  1. Resets to 0 at the start of each React render pass (in Streamdown)
  2. Each block's AnimatePlugin reads cursor.current as its startIndex before animating
  3. After animating, the plugin increments cursor.current by the number of newly animated words

This chains stagger delays automatically across all sibling blocks in render order without any manual setStartIndex wiring. Each block gets its own AnimatePlugin instance so prevContentLength tracking remains per-block independent.

Changes

  • animate.ts: Add AnimateCursor type + createAnimateCursor(), add cursor option to createAnimatePlugin(), fix missing lastRenderNewWordCount write
  • index.tsx: Replace single shared plugin with per-block plugins array + shared cursor; extract getBlockPlugins() helper to stay within biome complexity limits
  • animate.test.ts: Add 4 cursor chaining tests (36 tests total, all passing)

…nt concurrent animation

Previously all blocks shared a single animate plugin instance with
startIndex=0. When a new streaming block appeared, its words animated
at delay=0 concurrently with the previous block's still-animating words.

Fix: introduce AnimateCursor — a shared counter that resets to 0 before
each render pass. Each block gets its own AnimatePlugin connected to the
cursor; the plugin reads cursor.current as its startIndex, then increments
the cursor by the number of newly animated words. This automatically chains
stagger delays across all sibling blocks in render order.

Also fixes the missing lastRenderNewWordCount update in the rehype closure.

Closes vercel#482
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 2, 2026

@sleitor is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming animation reveals multiple markdown sections concurrently instead of serializing section animation

1 participant