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
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.tsships about 2.9KB of entirely static CSS this way (SIDENAV_CSSandSTACK_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 undergallery/appare legitimate and must not be changed:gallery/app/features/layout.tsgallery/app/global-error.tsgallery/app/features/route-handler/data/route.tsDesign / approach
Move
SIDENAV_CSSandSTACK_CSSinto the scaffold's ownpublic/input.csstemplate, which is compiled once topublic/tailwind.cssand 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. Itsapp-link-gc.tseven 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-stackflex-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: theSIDENAV_CSSandSTACK_CSSconsts (around L8 onward) and the<style>that emits them. Remove both and the endorsing comment.packages/cli/lib/create.js(see thepublic/input.csshandling around L198-L208, which composes the@webjsdev/uitheme CSS into it, and thecss:buildcommand at L314). The moved rules must land in whatever that writes, so they compile intopublic/tailwind.css.packages/cli/templates/public/input.csstemplate file exists, prefer editing it over the generator string; check which one is authoritative before editing, sincecreate.jscomposes 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 somx-autostill works. Moving it intoinput.csschanges 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.webjs check. Then actually visit/features/*and confirm the sidenav scrollbar behaviour and the demo section rhythm are unchanged.webjs-scaffold-syncskill: the scaffold has several surfaces that must move in lockstep (generators, templates, scaffold tests, the docs describing the scaffold).Invariants to respect:
htmltemplate body, including in CSS comments.webjs createoutput is the primary teaching surface for agents, so the comment that ships in it is part of the deliverable, not incidental.Tests + docs surfaces:
test/scaffolds/covers the generated app; add or extend an assertion that no page or layout undergallery/appemits a<style>, excluding the two exempt files listed above. That is the property this establishes.AGENTS.md/.agents/skills/webjs/references/styling.mdabout where static CSS belongs, the scaffold's copies underpackages/cli/templates/must match.Acceptance criteria
gallery/app/features/layout.tsemits no<style>; its rules are in the compiled stylesheetglobal-error.tsand the route handler are left alonewebjs check, and the/features/*sidenav scrollbar and demo spacing are visually unchanged<style>, with a counterfactual proving it fires