Skip to content

feat: emit enterFrom classes from SSR when intro is set#37

Merged
jonlaing merged 1 commit into
mainfrom
feat/ssr-emit-enter-from
Jul 16, 2026
Merged

feat: emit enterFrom classes from SSR when intro is set#37
jonlaing merged 1 commit into
mainfrom
feat/ssr-emit-enter-from

Conversation

@jonlaing

Copy link
Copy Markdown
Owner

Summary

Fix the FOUC on `intro: true`. Previously:

  1. SSR renders intro-animated content in its final visual state.
  2. Browser paints — user sees final state.
  3. Hydration runs, applies `enterFrom` classes (e.g. `opacity-0`), user sees content disappear.
  4. Enter animation transitions from `enterFrom` → final. User sees content fade back in.

The flash between steps 2 and 3 is the FOUC.

Now:

  1. SSR renders content with `enterFrom` classes already applied — browser paints the pre-animation state (invisible).
  2. Hydration's `runEnterAnimation` removes `enterFrom`, transitions to final state.

Same lifecycle as pure client-side rendering, no flash.

Plumbing

  • `SSRControlCtx.addSlot` reads `AnimationConfigCtx` after rendering each slot.
  • If `config.intro === true`, it extracts `enterFrom` from `config.list ?? config.single` and calls `renderer.toggleClass(element, cls, true)` for each class name.
  • Uses the renderer's own class primitive (not `Element.addClass`, which relies on DOM `classList`) so it works with the SSR StringRenderer's VNode model.
  • Only fires when `intro: true` — ordinary SSR output is unchanged.

Test plan

  • `npx tsc --noEmit` clean
  • Three new tests in `server.test.ts`:
    • `each({ intro: true })` → SSR HTML contains `enterFrom` classes on every item
    • `each({ ... })` without intro → SSR HTML has no `enterFrom` classes (verifies opt-in)
    • `when({ intro: true })` → SSR emits `enterFrom` on the rendered branch (single-slot case)
  • Full suite: 723 pass / 2 skipped (was 720 / 2)
  • Manual verification on the portfolio site — the flash should be gone

Note on user CSS

For this to give the expected visual result, users need the animation's transition to be set up in CSS. The recommended pattern (which the JSDoc examples use) is:

```ts
animate: {
enterFrom: "opacity-0 translate-y-4",
enter: "opacity-100 translate-y-0 transition duration-300",
}
```

The `transition` on `enter` is what makes the class-swap animate rather than snap.

🤖 Generated with Claude Code

SSR/SSG previously rendered intro-animated content in its final visual state. Then hydration would apply the enterFrom classes and transition — but between the browser's first paint and hydration starting, users saw a flash of the final state.

Now the SSR ControlCtx reads AnimationConfigCtx and, when intro: true is set, emits the enterFrom classes on each rendered slot element via the renderer's toggleClass primitive. The browser paints the pre-animation state directly, then hydration's runEnterAnimation removes enterFrom and transitions to the final state — same lifecycle as client-side, no flash.

Applies to each (per-item) and single-slot controls (when, match, etc). Ordinary non-intro SSR output continues to render the final state directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying effex with  Cloudflare Pages  Cloudflare Pages

Latest commit: 65c74ec
Status: ✅  Deploy successful!
Preview URL: https://ce1cfc68.effex.pages.dev
Branch Preview URL: https://feat-ssr-emit-enter-from.effex.pages.dev

View logs

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying effex-api with  Cloudflare Pages  Cloudflare Pages

Latest commit: 65c74ec
Status: ✅  Deploy successful!
Preview URL: https://0654de3a.effex-api.pages.dev
Branch Preview URL: https://feat-ssr-emit-enter-from.effex-api.pages.dev

View logs

@jonlaing
jonlaing merged commit da153bc into main Jul 16, 2026
4 checks passed
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.

1 participant