Problem
each's hydration path (correctly) attaches handlers to existing SSR/SSG DOM without re-running enter animations. This is the right default for content lists (feeds, sidebars, todos) — you don't want every list re-animating on every page load.
But for decorative content — hero-section letter cascades, staggered entrance headers, opening-scene sequences — this default means the animation never runs. The DOM was already there when hydration attached, so runEnterAnimation is skipped.
The current workaround is to defer the content client-side: render an empty container in SSR, then push items into the list post-hydration so the animation fires. This works but forfeits the SEO/first-paint benefit of SSR for exactly the content that's most visually important.
Proposed API
Add an intro?: boolean flag on EachConfig:
each(letters, {
key: (l) => l.id,
render: (l) => $.span({}, $.of(l.char)),
animate: { enter: "letter-in", stagger: stagger(40) },
intro: true, // re-animate the pre-rendered DOM on hydration
});
When intro: true:
- Client-only render: behaves identically to
each today (no pre-existing DOM, animations fire on initial sync).
- SSR/SSG render: container is marked with a data attribute so the hydration runtime knows this is an intro list.
- Hydration: for slots found in DOM, apply
enterFrom classes immediately (to set up the pre-animation state), attach handlers as usual, then run runEnterAnimation per slot after finalizeContainer completes. Same visual effect as if the DOM had been empty and the animation ran client-side.
Default is intro: false (existing behavior).
FOUC prevention
Between the SSR HTML arriving and hydration running, the browser has painted the "final" state. If we then apply enterFrom classes and reveal, the user sees the pre-animation flash. To prevent this:
- Emit a small
<script> at the end of generateDocument that adds data-effex-pre-anim to [data-effex-intro-container] before the main hydration script runs.
- CSS rule (either in a shipped
@effex/dom stylesheet or documented as required): [data-effex-pre-anim] { visibility: hidden; }.
- Hydration removes the attribute right before running the enter animation.
If JS never loads, the attribute is never added → content stays visible. No-JS fallback preserved.
Considerations
- Reduced motion.
intro: true should respect prefers-reduced-motion — if enabled, skip the hide-and-reveal entirely and just attach handlers as normal each would.
- Post-hydration items. Once hydration completes,
intro: true behaves like ordinary each — new items added later get the standard enter animation via the post-hydration path. The intro flag only affects the first paint.
- Non-list controls. If this pattern proves useful for
when / match / etc. later, extend those with the same flag rather than adding new sibling functions.
Depends on
Semver
Minor on @effex/dom (additive config option). Potentially patch on @effex/platform for the FOUC-prevention script in generateDocument.
Problem
each's hydration path (correctly) attaches handlers to existing SSR/SSG DOM without re-running enter animations. This is the right default for content lists (feeds, sidebars, todos) — you don't want every list re-animating on every page load.But for decorative content — hero-section letter cascades, staggered entrance headers, opening-scene sequences — this default means the animation never runs. The DOM was already there when hydration attached, so
runEnterAnimationis skipped.The current workaround is to defer the content client-side: render an empty container in SSR, then push items into the list post-hydration so the animation fires. This works but forfeits the SEO/first-paint benefit of SSR for exactly the content that's most visually important.
Proposed API
Add an
intro?: booleanflag onEachConfig:When
intro: true:eachtoday (no pre-existing DOM, animations fire on initial sync).enterFromclasses immediately (to set up the pre-animation state), attach handlers as usual, then runrunEnterAnimationper slot afterfinalizeContainercompletes. Same visual effect as if the DOM had been empty and the animation ran client-side.Default is
intro: false(existing behavior).FOUC prevention
Between the SSR HTML arriving and hydration running, the browser has painted the "final" state. If we then apply
enterFromclasses and reveal, the user sees the pre-animation flash. To prevent this:<script>at the end ofgenerateDocumentthat addsdata-effex-pre-animto[data-effex-intro-container]before the main hydration script runs.@effex/domstylesheet or documented as required):[data-effex-pre-anim] { visibility: hidden; }.If JS never loads, the attribute is never added → content stays visible. No-JS fallback preserved.
Considerations
intro: trueshould respectprefers-reduced-motion— if enabled, skip the hide-and-reveal entirely and just attach handlers as normaleachwould.intro: truebehaves like ordinaryeach— new items added later get the standard enter animation via the post-hydration path. Theintroflag only affects the first paint.when/match/ etc. later, extend those with the same flag rather than adding new sibling functions.Depends on
Semver
Minor on
@effex/dom(additive config option). Potentially patch on@effex/platformfor the FOUC-prevention script ingenerateDocument.