Skip to content

fix: remove animation spans after streaming ends - #573

Open
BetterAndBetterII wants to merge 2 commits into
vercel:mainfrom
BetterAndBetterII:fix/animation-spans-settled-text
Open

fix: remove animation spans after streaming ends#573
BetterAndBetterII wants to merge 2 commits into
vercel:mainfrom
BetterAndBetterII:fix/animation-spans-settled-text

Conversation

@BetterAndBetterII

Copy link
Copy Markdown

Description

Fix animation spans (data-sd-animate) persisting in the DOM after isAnimating goes false. Memoized leaf components (MemoParagraph, MemoLi, etc.) compare only className + source position and never compare children, so they discard the span-free reparse that Block produces when the animate rehype plugin is removed from the pipeline.

Root cause

Block comparator correctly detects the rehype pipeline change and re-parses without the animate plugin. But the resulting span-free HAST never commits because leaf memo comparators (sameClassAndNode) see identical class + position and return "equal" — React throws away the new children.

Fix

Include isAnimating in block keys. When streaming ends, keys change and React mounts fresh subtrees, allowing the span-free reparse to commit. During streaming (isAnimating stays true), keys are stable — no hot-path penalty.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Related Issues

Fixes #570

Changes Made

  • packages/streamdown/index.tsx: Include isAnimating in block key computation so React remounts fresh subtrees when the animate rehype plugin is removed
  • packages/streamdown/__tests__/animate-integration.test.tsx: New integration test verifying spans are removed when isAnimating goes false
  • .changeset/clean-settled-animation-spans.md: Patch changeset

Testing

  • All existing tests pass (983/983, 74 files)
  • Added new integration test: animate-integration.test.tsx
  • RED confirmed: test fails without fix (19 spans persist)
  • GREEN confirmed: test passes with fix (0 spans)
  • No regressions in full test suite

Checklist

  • My code follows the project code style
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective
  • New and existing unit tests pass locally with my changes
  • I have created a changeset for these changes

Additional Notes

The fix only affects the transition when isAnimating flips. During active streaming (isAnimating stays true), block keys remain stable — no extra remounting on the hot path. The one-time remount cost when streaming ends is negligible compared to the persistent DOM pollution of stale animation spans.

@vercel

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

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

A member of the Team first needs to authorize it.

@vercel vercel Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

blockKeys unconditionally includes isAnimating in every block key, forcing a full unmount/remount of all blocks when streaming ends — even when the animated prop is absent and there are no animation spans to discard.

Fix on Vercel

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.

Animation spans are never removed from settled text — memoized components discard the span-free reparse

1 participant