Skip to content

Add intro flag to each for hydration re-animation #27

Description

@jonlaing

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

  1. 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.
  2. 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.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions