Skip to content

scaffold: gallery layout ships static CSS in a swapped page/layout style block #1110

Description

@vivek7405

Problem

The scaffold teaches every generated app to put static CSS in a swapped page/layout <style>, which mutates the document CSSOM on every navigation into or out of that route. See #1109 for the mechanism and the flash it can produce on preserved layouts.

packages/cli/templates/gallery/app/features/layout.ts ships about 2.9KB of entirely static CSS this way (SIDENAV_CSS and STACK_CSS), and its comment actively endorses the pattern:

// <style> (it never hydrates, unlike a component), so this is legitimate here.

That reasoning is correct about hydration and wrong about navigation. A <style> inside a swapped boundary is added or removed on every crossing, forcing a document-wide style recalculation. Because the gallery is the primary teaching surface for AI agents, this comment is likely to be copied into real apps as a justification.

Precisely scoped, this is the ONLY offending instance in the scaffold. The other two <style> blocks under gallery/app are legitimate and must not be changed:

File Verdict
gallery/app/features/layout.ts In a swap range. The instance to fix
gallery/app/global-error.ts Exempt. Root-only boundary that renders its OWN document verbatim with no importmap or boot script, so it is never swapped and cannot use the compiled stylesheet
gallery/app/features/route-handler/data/route.ts Exempt. A route handler returning an HTML fragment, not a layout

Design / approach

Move SIDENAV_CSS and STACK_CSS into the scaffold's own public/input.css template, which is compiled once to public/tailwind.css and loaded by the root layout, and correct the misleading comment.

Prior art worth following (see #1109 for detail): Next.js never emits page or layout CSS as a <style> in the swapped tree. It extracts CSS to files and renders <link rel="stylesheet" precedence="next">, which React hoists into <head> and dedupes by href, so an unchanged stylesheet is untouched across navigation. Its app-link-gc.ts even delays removing a superseded stylesheet by 5ms with the comment "Delay the removal of the stylesheet to avoid FOUC", which is direct confirmation that swapping stylesheets during navigation is a known flash source.

The CSS itself is fine and should not be converted to utilities: both blocks style things utilities cannot express (webkit scrollbar pseudo-elements, and a .demo-stack flex-gap rhythm that deliberately zeroes block-axis margins). The problem is where the rules live, not that they exist.

Implementation notes (for the implementing agent)

Where to edit:

  • packages/cli/templates/gallery/app/features/layout.ts: the SIDENAV_CSS and STACK_CSS consts (around L8 onward) and the <style> that emits them. Remove both and the endorsing comment.
  • The scaffold's stylesheet input is written by packages/cli/lib/create.js (see the public/input.css handling around L198-L208, which composes the @webjsdev/ui theme CSS into it, and the css:build command at L314). The moved rules must land in whatever that writes, so they compile into public/tailwind.css.
  • If a packages/cli/templates/public/input.css template file exists, prefer editing it over the generator string; check which one is authoritative before editing, since create.js composes part of this file at generate time.

Landmines / gotchas:

  • STACK_CSS's comment explains it is deliberately UNLAYERED so it beats Tailwind's layered utilities, and that it zeroes only block-axis margins so mx-auto still works. Moving it into input.css changes its cascade position relative to Tailwind's layers. Verify the demo spacing still looks right rather than assuming; this is the rule most likely to break silently.
  • The generators emit strings, so an escaping bug only shows in a freshly generated app. Do not review the template in isolation.
  • Mandatory verification for any scaffold change: generate an app, boot it, and run webjs check. Then actually visit /features/* and confirm the sidenav scrollbar behaviour and the demo section rhythm are unchanged.
  • Invoke the webjs-scaffold-sync skill: the scaffold has several surfaces that must move in lockstep (generators, templates, scaffold tests, the docs describing the scaffold).

Invariants to respect:

  • Invariant 9: no backticks inside an html template body, including in CSS comments.
  • AGENTS.md scaffold rules: webjs create output is the primary teaching surface for agents, so the comment that ships in it is part of the deliverable, not incidental.

Tests + docs surfaces:

Acceptance criteria

  • gallery/app/features/layout.ts emits no <style>; its rules are in the compiled stylesheet
  • The misleading "it never hydrates, so this is legitimate here" comment is gone or corrected
  • global-error.ts and the route handler are left alone
  • A generated app boots, passes webjs check, and the /features/* sidenav scrollbar and demo spacing are visually unchanged
  • A scaffold test asserts no swapped page/layout emits a <style>, with a counterfactual proving it fires

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Status
Todo

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions