Skip to content

fix: prevent ordered list animation retrigger during streaming#417

Open
sleitor wants to merge 2 commits intovercel:mainfrom
sleitor:fix/ordered-list-animation-retrigger
Open

fix: prevent ordered list animation retrigger during streaming#417
sleitor wants to merge 2 commits intovercel:mainfrom
sleitor:fix/ordered-list-animation-retrigger

Conversation

@sleitor
Copy link
Contributor

@sleitor sleitor commented Feb 22, 2026

Summary

Fixes #410 — Ordered list animations incorrectly retrigger when new content appears during streaming.

Root Cause

When multiple lists share a single parsed block (because marked Lexer merges consecutive list items into one token), streaming updates cause the entire block to re-render through the rehype pipeline. The animate plugin previously created fresh <span data-sd-animate> elements for all text nodes on every render, causing already-visible content to re-animate with a visible flicker.

Solution

  • Track previous content length per Block via useRef
  • Pass prevContentLength to the animate plugin before each synchronous render
  • Characters within the previous content length get --sd-duration:0ms (instant final state with no visible animation)
  • Only new characters receive the configured animation duration

This approach is similar in spirit to the code block highlight flicker fix in 0987479 — both prevent re-processing of already-rendered content during streaming updates.

Changes

  • packages/streamdown/lib/animate.ts: Added prevContentLength tracking, setPrevContentLength() and resetPrevContentLength() methods on AnimatePlugin
  • packages/streamdown/index.tsx: Block component tracks previous content length via useRef and configures animate plugin before each render

Testing

  • All 491 existing tests pass
  • Build succeeds

@vercel
Copy link
Contributor

vercel bot commented Feb 22, 2026

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

A member of the Team first needs to authorize it.

…n length

The animate plugin's charCounter counts HAST text node characters (rendered
text, without markdown syntax). Previously, prevContentLengthRef stored
content.length (raw markdown), causing a unit mismatch: markdown syntax
characters (**, #, `, etc.) inflate the raw length vs the HAST count.

This mismatch caused new streaming content to incorrectly skip animation
when prevContentLength (raw) exceeded the actual HAST character count.

Fix: expose getLastRenderCharCount() on AnimatePlugin that returns the
total HAST character count from the last render. Block now uses this value
instead of content.length so both sides measure the same units.

Also fix lint issues in list-animation-retrigger.test.tsx:
- Replace async () => {} with () => Promise.resolve() for empty act() calls
- Remove async from act callbacks that don't use await
- Remove unused renderCount variable
@sleitor sleitor force-pushed the fix/ordered-list-animation-retrigger branch from 1196045 to ef64268 Compare February 22, 2026 15:17
The previous implementation called resetPrevContentLength() in Block's
function body, but Markdown (which calls processor.runSync synchronously)
renders as a child component — AFTER Block's function body returns. This
meant the animate plugin always saw prevContentLength=0 on every render.

Fix:
- Remove manual resetPrevContentLength() from Block's render body
- Add self-reset inside rehypeAnimate after each run, so sibling blocks
  start clean (depth-first rendering ensures Markdown1 runs before Block2)
- Read getLastRenderCharCount() at the TOP of Block's render body: since
  React renders depth-first, this value is from the PREVIOUS Markdown run
  (exactly the prevContentLength needed for the current render)
- Remove stale useLayoutEffect approach (not needed with depth-first timing)
@sleitor sleitor marked this pull request as draft February 22, 2026 18:59
@sleitor sleitor marked this pull request as ready for review February 22, 2026 19:35
@sleitor sleitor force-pushed the fix/ordered-list-animation-retrigger branch from 6b7997e to 958cdf8 Compare February 22, 2026 22:21
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.

Ordered list animations incorrectly retrigger

1 participant