Progressive rendering with DOM morphing for generative UI#40
Progressive rendering with DOM morphing for generative UI#40GeneralJerel merged 2 commits intomainfrom
Conversation
) Replace innerHTML with idiomorph DOM morphing in WidgetRenderer to eliminate flickering during streaming. Existing DOM nodes are preserved across updates — only diffs are applied. New elements fade in via .morph-enter animation; first render uses staggered entrance. Also enable Recharts entrance animations on bar/pie charts and add staggered fade-in to MeetingTimePicker time slots. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GeneralJerel
left a comment
There was a problem hiding this comment.
Review
Overall: Good approach — replacing innerHTML with idiomorph DOM morphing is the right call to fix streaming flicker. The changes are focused and the fallback path is sensible. A few issues worth addressing:
Issues
1. Inlined minified library is a maintenance burden — widget-renderer.tsx:334-337
The IDIOMORPH_JS constant is ~4KB of minified code inlined as a string literal. This makes the file harder to read, impossible to diff on library updates, and there's no way to verify it matches the claimed v0.3.0. Consider installing idiomorph as a dependency and bundling it, or at minimum adding a script that regenerates this constant from the npm package so it can be verified/updated.
2. morph-enter class is never removed — widget-renderer.tsx:390-394
The beforeNodeAdded callback adds morph-enter to every new element node, but nothing ever removes it. Since the animation uses both fill mode, the visual result is fine, but:
- The class accumulates on every element across morphs
- If the CSS were ever changed (e.g., removing
both), stale classes would cause bugs - It adds noise when inspecting the DOM
Consider removing the class after the animation completes (e.g., via an animationend listener).
3. Inline <style> in MeetingTimePicker JSX — meeting-time-picker.tsx:49-54
Injecting a <style> tag directly in the component body means it's re-inserted on every render. This keyframe is generic enough to live in a shared CSS file or as a Tailwind @keyframes definition. It also duplicates the concept already in widget-renderer.tsx's fadeSlideIn.
4. No data-has-content reset path — widget-renderer.tsx:377-382
Once data-has-content is set, the widget can never get the staggered entrance animation again (e.g., if the content is cleared and a new streaming session starts in the same iframe). If the iframe is always recycled between sessions this is fine, but worth confirming.
Minor / Nits
- Chart animation props look good. The 200ms
animationBegindelay on pie gives a nice sequenced feel. - The
prefers-reduced-motionmedia query correctly respects accessibility — nice. - The
initial-renderclass removal timeout (800ms) matches the animation budget well.
Verdict
The core approach is solid. Issues #1 and #2 are worth fixing before merge; #3 and #4 are minor improvements that could be follow-ups.
… animations Extract inlined idiomorph minified code to dedicated module with update instructions. Add animationend listener to remove morph-enter class after animation completes. Move MeetingTimePicker fadeSlideUp keyframes from inline <style> to globals.css. Reset data-has-content on empty content so new streaming sessions get the staggered entrance animation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Closes #33
innerHTMLwith idiomorph DOM morphing to eliminate flickering during streaming. Existing DOM nodes are preserved across updates — only diffs are applied. New elements fade in via.morph-enteranimation; first render uses staggered entrance.Test plan
🤖 Generated with Claude Code