Skip to content

fix(animate): rework streaming animation into a staged reveal#531

Open
isaacwasserman wants to merge 2 commits into
vercel:mainfrom
isaacwasserman:revert-streamdown-renames
Open

fix(animate): rework streaming animation into a staged reveal#531
isaacwasserman wants to merge 2 commits into
vercel:mainfrom
isaacwasserman:revert-streamdown-renames

Conversation

@isaacwasserman
Copy link
Copy Markdown

Description

Problem 1 (#482) — sections animated on top of each other. While a list was still
fading in, the next paragraph or heading would already start animating, so
several parts of the message were “alive” at once and the reveal felt chaotic.

Problem 2 — rich content animated out of order, or not at all. With
formatting like bold, italics, or links, often only the last word animated while
everything before it popped in instantly, so the reveal looked random.

Problem 3 — Despite animating in word-by-word, the layout (height of the markdown component) adjusts immediately, causing large amounts of chunkily growing foot space.

How it’s fixed:

  • The animation now treats the whole message as one ordered sequence and reveals it a chunk at a time. Each word fades in on its turn, top to bottom, regardless of formatting. In other words, section i always animates before section i+1, no matter when they were added to the queue.
  • Additionally, rather than purely animating the opacity of text, by default we now animate the display CSS property to prevent hidden segments from impacting layout.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Related Issues

Fixes #482
Related to #493

Changes Made

  • Streaming content now reveals as one global, ordered sequence — section i always animates before section i+1, so sibling sections no longer animate at the same time.
  • Every word animates in order regardless of formatting (bold, italics, links); code blocks, images, and math reveal as a single unit.
  • Unrevealed segments collapse via display instead of only fading opacity, so the component grows in step with the reveal instead of reserving the full height up front.
  • Already-revealed content reconciles in place — no remounting or re-animation as more text streams in.
  • Added an opt-in animated={{ reserveSpace: true }} to keep the previous opacity-only behavior (reserve layout for unrevealed content).
  • Updated the animation docs. Public API (the animated prop, createAnimatePlugin, CSS classes/keyframes) is unchanged.

Testing

  • All existing tests pass
  • Added new tests for the changes
  • Manually tested the changes

Test Coverage

974 tests passing (77 files). New suites cover the reveal transform, the scheduler, an end-to-end streamed reveal, StrictMode/concurrent idempotency, and the reserveSpace toggle; the list re-animation regression test was rewritten for the new model.

Screenshots/Demos

Checklist

  • My code follows the project's code style
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have created a changeset (pnpm changeset)

Changeset

  • I have created a changeset for these changes

Additional Notes

  • No public API changes. The behavior change is non-breaking: when animated is enabled, content is wrapped in lightweight keyed spans that persist for stable reconciliation, and unrevealed text is collapsed with display: none by default (use reserveSpace: true for the previous opacity-only layout). Inline code now animates with the surrounding text.
  • This is an alternative approach to fix(animate): serialize stagger delays across sibling blocks to prevent concurrent animation #493: instead of per-block plugins chained by a shared cursor, it keeps one shared plugin and orders all blocks through a single controller over a global segment sequence.

Replace the per-word "animate newly-mounted words" model with a single
controller-driven reveal over a global, document-ordered segment space:

- Serialize sibling sections: exactly one chunk animates at a time across
  all blocks, so a list finishes revealing before the next paragraph starts.
- Order every segment with the correct staggered delay, including rich/inline
  content; atomic elements (code, images, math, embeds) reveal as a unit.
- Stable per-segment keys reconcile already-shown content in place instead of
  remounting or re-animating it when the markdown re-parses mid-stream.
- Step the controller idempotently during render, so the reveal no longer
  snaps under React StrictMode or concurrent rendering.
- Add `animated={{ reserveSpace: true }}` to fade segments in place without
  layout shift instead of the default display:none collapse.

Internals live under lib/animate/ (transform, controller, plugin,
segment-counter, use-animation) with one consistent "animate" vocabulary.
Docs and tests updated; the public API (createAnimatePlugin, the animated
prop, data-sd-* attributes, sd-* keyframes) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Jun 3, 2026

Someone is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread packages/streamdown/lib/animate/use-animation.ts
The block segment counter was only rebuilt on animate-config changes, so a
mid-stream change to the component/plugin set left it parsing a stale tree and
returning stale cached counts — misaligning the global segment ordinals. Rebuild
the counter (clearing its cache) whenever the config OR countingOptions identity
changes.

Also extract pure helpers (computeBaseOrdinals, toPerBlock, latchNow) to keep
useAnimation under the cognitive-complexity limit, and add a regression test that
drives the hook through a countingOptions change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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