Skip to content

✨ feat(markdown): implement streaming animation for markdown blocks#475

Merged
Innei merged 3 commits intomasterfrom
new/stream-v2
Mar 3, 2026
Merged

✨ feat(markdown): implement streaming animation for markdown blocks#475
Innei merged 3 commits intomasterfrom
new/stream-v2

Conversation

@Innei
Copy link
Member

@Innei Innei commented Feb 18, 2026

Summary

  • Implement streaming animation for markdown blocks with smooth reveal effects
  • Simplify StreamdownBlock and StreamdownRender components for better maintainability

Commits

  • 86812c94 ✨ feat(markdown): implement streaming animation for markdown blocks
  • d6ed6e0e ♻️ refactor(markdown): simplify StreamdownBlock and StreamdownRender components

Test plan

  • Verify markdown streaming animation displays correctly
  • Check that StreamdownBlock and StreamdownRender components render properly
  • Ensure no visual regressions in existing markdown rendering

Summary by Sourcery

Add per-block streaming animation for markdown rendering with dynamic timing and simplified components.

New Features:

  • Introduce character-level streaming animation for markdown blocks using a dedicated rehype plugin and queue logic.

Enhancements:

  • Refactor StreamdownRender to operate on structured markdown blocks with state-driven animation phases.
  • Simplify StreamdownBlock memoization and decouple streaming animation from the global markdown rehype plugin list.
  • Add a useStreamQueue hook to manage block reveal order, timing, and streaming states for markdown content.
  • Adjust markdown animation styles to use a dedicated .stream-char class and shorter fade timing for smoother reveals.

@vercel
Copy link

vercel bot commented Feb 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lobe-ui Error Error Mar 3, 2026 9:32am

Request Review

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 18, 2026

Reviewer's Guide

Implements a streaming character-by-character reveal animation for markdown blocks by introducing a queue-based block state machine, a configurable rehype plugin for per-character spans with staggered animations, and wiring it into StreamdownRender with new styling, while removing the old global animated flag and simplifying block rendering.

Sequence diagram for streaming markdown rendering and animation

sequenceDiagram
  actor User
  participant StreamdownRender
  participant useStreamQueue
  participant rehypeStreamAnimated
  participant DOM

  User->>StreamdownRender: provide_markdown_children
  StreamdownRender->>StreamdownRender: useMarkdownContent
  StreamdownRender->>StreamdownRender: remend_content
  StreamdownRender->>StreamdownRender: marked_lexer_to_BlockInfo

  StreamdownRender->>useStreamQueue: useStreamQueue(blocks)
  useStreamQueue-->>StreamdownRender: getBlockState charDelay queueLength

  loop for_each_block
    StreamdownRender->>useStreamQueue: getBlockState(index)
    useStreamQueue-->>StreamdownRender: BlockState

    alt state_is_streaming
      StreamdownRender->>rehypeStreamAnimated: configure(baseCharCount charDelay)
    else state_is_animating
      StreamdownRender->>rehypeStreamAnimated: configure(baseCharCount0 charDelay)
    else state_is_revealed
      StreamdownRender->>rehypeStreamAnimated: not_used
    end

    StreamdownRender->>DOM: render_StreamdownBlock_with_plugins
  end

  rehypeStreamAnimated->>DOM: wrap_chars_in_span_stream_char
  DOM-->>User: sees_character_by_character_reveal
Loading

Class diagram for streaming markdown rendering components

classDiagram
  direction LR

  class StreamdownRender {
    +children string
    +render()
    +useMarkdownContent(children)
    +useMarkdownComponents()
    +useMarkdownRehypePlugins()
    +useMarkdownRemarkPlugins()
    +useStreamQueue(blocks)
  }

  class StreamdownBlock {
    +children string
    +render()
  }

  class BlockInfo {
    +content string
    +startOffset number
  }

  class BlockState {
    <<enumeration>>
    revealed
    animating
    streaming
    queued
  }

  class UseStreamQueueReturn {
    +charDelay number
    +queueLength number
    +getBlockState(index)
  }

  class useStreamQueue {
    +useStreamQueue(blocks)
    -revealedCount number
    -minRevealedRef number
    -prevBlocksLenRef number
    -timerRef Timeout
    +getBlockState(index)
  }

  class StreamAnimatedOptions {
    +baseCharCount number
    +charDelay number
  }

  class rehypeStreamAnimated {
    +rehypeStreamAnimated(options)
    -hasClass(node, cls)
    -wrapText(node)
    -shouldSkip(node)
  }

  class styles_animated {
    <<style>>
    +stream_char opacity0
    +stream_char animationFadeIn
  }

  StreamdownRender --> StreamdownBlock : renders
  StreamdownRender --> BlockInfo : computes_blocks
  StreamdownRender --> useStreamQueue : uses
  StreamdownRender --> rehypeStreamAnimated : configures_plugins
  StreamdownRender --> UseStreamQueueReturn : receives_state

  useStreamQueue --> BlockInfo : manages_queue_for
  useStreamQueue --> BlockState : returns

  rehypeStreamAnimated --> StreamAnimatedOptions : configured_by

  styles_animated --> rehypeStreamAnimated : targets_stream_char
Loading

File-Level Changes

Change Details Files
Refactor StreamdownRender to tokenize markdown into blocks, drive a streaming state machine per block, and vary rehype plugins based on block state.
  • Use marked.lexer directly in StreamdownRender to build BlockInfo objects with content and startOffset instead of a separate parse helper
  • Introduce useStreamQueue to compute per-block state (queued/streaming/animating/revealed) and expose a dynamic charDelay
  • Compute different rehype plugin lists per block based on its state, wiring in rehypeStreamAnimated with appropriate baseCharCount and charDelay
  • Track previous streaming block character count and offset via refs to keep animations continuous across renders and block growth
  • Simplify StreamdownBlock to a memoized Markdown wrapper without custom props comparison and use more stable keys based on block.startOffset
src/Markdown/SyntaxMarkdown/StreamdownRender.tsx
src/Markdown/SyntaxMarkdown/useStreamQueue.ts
Introduce rehypeStreamAnimated as a per-character animation plugin with options for baseCharCount and charDelay, including safeguards for code, tables, and math.
  • Define StreamAnimatedOptions interface with baseCharCount and charDelay configuration
  • Wrap text nodes within selected block-level tags into span.stream-char elements with per-character animation-delay style computed from globalCharIndex and baseCharCount
  • Skip animation for specific tags (pre, code, table, svg) and elements marked as Katex to avoid breaking formatting and math rendering
  • Maintain a globalCharIndex counter across the tree so delays are cumulative, while baseCharCount allows resuming from a prior streaming position
src/Markdown/plugins/rehypeStreamAnimated.ts
Add a stream-specific animation style and decouple animated behavior from the shared markdown rehype plugin hook.
  • Define .stream-char CSS to start at opacity 0 and play a short fadeIn animation with ease-out timing and forwards fill mode
  • Preserve and hard-disable animation side effects inside Katex display spans to avoid unintended masking/animation
  • Remove rehypeStreamAnimated wiring from useMarkdownRehypePlugins and the animated flag dependency so streaming is controlled solely by StreamdownRender
src/Markdown/SyntaxMarkdown/style.ts
src/hooks/useMarkdown/useMarkdownRehypePlugins.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@lobehubbot
Copy link
Member

👍 @Innei


Thank you for raising your pull request and contributing to our Community
Please make sure you have followed our contributing guidelines. We will review it as soon as possible.
If you encounter any problems, please feel free to connect with us.
非常感谢您提出拉取请求并为我们的社区做出贡献,请确保您已经遵循了我们的贡献指南,我们会尽快审查它。
如果您遇到任何问题,请随时与我们联系。

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 18, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@lobehub/ui@475

commit: a48d541

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The countChars helper is duplicated in both StreamdownRender and useStreamQueue; consider extracting this into a shared utility to keep the character-counting logic consistent and easier to maintain.
  • There are now two sources of character delay (STREAM_CHAR_DELAY in StreamdownRender and the computed charDelay from useStreamQueue used in staggerPlugins); it would be clearer to unify these so the streaming and animating states derive delay from a single, well-defined configuration.
  • In rehypeStreamAnimated, you are building inline style strings for animation-delay; if more style properties are added later this string-based approach can get brittle—consider using a structured style object (or merging with existing styles) to avoid accidentally overwriting other inline styles.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `countChars` helper is duplicated in both `StreamdownRender` and `useStreamQueue`; consider extracting this into a shared utility to keep the character-counting logic consistent and easier to maintain.
- There are now two sources of character delay (`STREAM_CHAR_DELAY` in `StreamdownRender` and the computed `charDelay` from `useStreamQueue` used in `staggerPlugins`); it would be clearer to unify these so the streaming and animating states derive delay from a single, well-defined configuration.
- In `rehypeStreamAnimated`, you are building inline `style` strings for `animation-delay`; if more style properties are added later this string-based approach can get brittle—consider using a structured `style` object (or merging with existing styles) to avoid accidentally overwriting other inline styles.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Innei added 3 commits March 3, 2026 17:10
- Introduced `useStreamQueue` hook to manage streaming state and delays for markdown blocks.
- Enhanced `StreamdownRender` to utilize streaming animations with staggered effects.
- Updated styles for animated streaming characters.
- Added `rehypeStreamAnimated` plugin to handle character-level animations in markdown rendering.

This update improves the user experience by providing a more dynamic and engaging way to display markdown content.

Signed-off-by: Innei <tukon479@gmail.com>
…components

- Refactored the `StreamdownBlock` and `StreamdownRender` components for improved readability and maintainability.
- Removed unnecessary memoization in `StreamdownBlock`.
- Streamlined the use of `useEffect` and `useMemo` hooks in `StreamdownRender` to enhance performance.
- Ensured consistent handling of previous character counts and stream offsets.

This refactor lays the groundwork for future enhancements in markdown rendering.

Signed-off-by: Innei <tukon479@gmail.com>
- Introduced `revealed` option in `rehypeStreamAnimated` plugin to control character visibility during streaming.
- Updated `StreamdownRender` to utilize stable plugin references for improved performance.
- Added utility functions for deep comparison of plugins to prevent unnecessary re-renders.
- Enhanced styles for revealed characters to ensure smooth transitions.

This update improves the markdown rendering experience by providing more control over streaming animations and optimizing component performance.

Signed-off-by: Innei <tukon479@gmail.com>
@Innei Innei merged commit 2b3d230 into master Mar 3, 2026
5 of 6 checks passed
@Innei Innei deleted the new/stream-v2 branch March 3, 2026 09:45
@lobehubbot
Copy link
Member

❤️ Great PR @Innei ❤️


The growth of project is inseparable from user feedback and contribution, thanks for your contribution!
项目的成长离不开用户反馈和贡献,感谢您的贡献!

github-actions bot pushed a commit that referenced this pull request Mar 3, 2026
# [5.2.0](v5.1.1...v5.2.0) (2026-03-03)

### ✨ Features

* **markdown**: Implement streaming animation for markdown blocks, closes [#475](#475) ([2b3d230](2b3d230))
@lobehubbot
Copy link
Member

🎉 This PR is included in version 5.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants