Inline Style In Body
DL#164 — Inline Style Blocks in Body
Written for AI agents. See Log Methodology Note below for details.
Background
Jay's template parser scans <body> content for {...} expressions (template bindings like {post.title}). This works because HTML content doesn't normally contain curly braces.
CSS uses curly braces for rule blocks ({ transform: translateY(0); }). When a <style> tag appears in <body>, the parser treats CSS curly braces as template expressions and fails.
How this happens
The AIditor (design tool) generates inline <style> tags in the body for component-scoped animations:
<body>
<div class="hero">
<style>
@keyframes fall {
0% {
transform: translateY(-6px);
opacity: 0.4;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.hero-particle {
animation: fall 2s ease-in-out infinite;
}
</style>
<div class="hero-particle">...</div>
</div>
</body>
The parser sees { transform: translateY(-6px); opacity: 0.4; } and tries to parse it as a template binding expression.
Current behavior
The <head> already handles <style> correctly — the parser extracts CSS from head styles and processes them separately. But <body> styles are scanned for template bindings like any other text content.
Error
Failed to parse expression [
@keyframes fall {
0% { transform: translateY(-6px); opacity: 0.4; ].
Parse error: Expected "." or identifier but "\n" found.
Related Design Logs
- DL#162 — Structural headfull components (template injection)
- DL#146 — CSS performance fixes
Problem
- Parser crash — inline
<style>in body causes a hard failure, not a graceful fallback - No support for body styles — even if the parser didn't crash, body
<style>content would be treated as text nodes and mangled - Design tool output — the AIditor legitimately generates
<style>in body for scoped animations. The framework should handle this.
Design
Three concerns
- Body
<style>crashes the parser — CSS curly braces are parsed as template bindings - Body
<style>for inline co-located styles — design tool output, animations - Headfull component CSS scoping — component styles from
<head>leak into the global scope
Priority 1: Don't crash
The parser must skip <style> tags in <body> during template expression scanning. CSS content should never be parsed as template bindings. This is a bug regardless of any design decision.
Priority 2: Support inline body styles
Inline <style> in <body> is emitted as-is in the rendered output. The CSS is not hoisted, not extracted, not deduplicated — it stays exactly where the designer placed it.
<body>
<section class="hero">
<style>
@keyframes fall {
0% {
transform: translateY(-6px);
opacity: 0.4;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
.hero-particle {
animation: fall 2s ease-in-out infinite;
}
</style>
<div class="hero-particle">...</div>
</section>
</body>
All three compiler targets treat <style> in body as an opaque node:
- Server element: emit as raw HTML string
- Element target: emit as static element (no reactivity)
- Hydrate target: skip (static content, no coordinates needed)
Priority 3: Headfull component CSS scoping with @scope
Headfull component styles (extracted from the component's <head>) currently merge into the page's global CSS unscoped. This causes class name collisions — a SiteHeader's .nav-link leaks into the page.
Auto-scope with @scope: when extracting headfull component CSS during template injection, wrap it in @scope targeting the component's root element:
/* Before (current — global, leaks) */
.header-brand {
font-size: 32px;
}
.nav-link {
color: var(--text-muted);
}
/* After (auto-scoped by the compiler) */
@scope ([jc="site-header"]) {
.header-brand {
font-size: 32px;
}
.nav-link {
color: var(--text-muted);
}
}
The compiler adds a jc="site-header" attribute to the component's root element in the injected template. The scoped CSS is merged into the page's extracted CSS — it goes through the same extraction, minification, and caching pipeline as all head styles.
Why @scope
- Proper CSS encapsulation as the spec intended
- No synthetic class names or compiler-generated selectors
- Lower specificity than class-based scoping
@scopeboundary matches the component's DOM root naturallytoboundary available for limiting scope depth if needed- Well supported: Chrome 118+, Firefox 128+, Safari 17.4+
What gets scoped
- Headfull component
<head>styles — auto-wrapped in@scopeduring CSS extraction - Body
<style>tags — emitted as-is (designer controls scoping manually) - Page-level
<head>styles — NOT scoped (global by intent)
@keyframes and :root
@keyframes defined inside @scope are still global (CSS spec). :root and body selectors inside @scope are ignored by the browser — component CSS shouldn't use them.
Since @keyframes can't be scoped, two components defining the same animation name (e.g., @keyframes fade) would silently collide — one overrides the other. The framework should detect this:
Validation: during CSS extraction, collect all @keyframes names across the page's own CSS and all headfull component CSS. If duplicates are found, emit a warning:
Warning: @keyframes "fade" is defined in both site-header and hero-section.
Animation names are global — rename one to avoid collisions (e.g., "site-header-fade").
@font-face has the same issue — two components declaring font-family: "Icons" with different src would silently collide:
Warning: @font-face "Icons" is defined in both site-header and product-card.
Font family names are global — rename one to avoid collisions.
Both are validation-only checks — no auto-prefixing. The designer renames the collision.
Priority 4: Validation guidance
Warn (not error) when body <style> uses broad selectors that could leak:
- Bare element selectors:
div { ... },p { ... }— likely unintentional global styles *selector — almost certainly wrong in body styles- No warning for class selectors, ID selectors,
@keyframes,@scope— these are intentional
Priority 5: Agent-kit guide
Document in designer/jay-html-styling.md:
- Page
<head>styles — global styles, design tokens, page-level layout. Extracted, minified, cached. - Component
<head>styles — auto-scoped by the framework using@scope. No manual scoping needed. - Body
<style>— co-located inline styles for animations, component-specific rules. Emitted as-is. - When to use which: page head for shared styles, component head for component styles (auto-scoped), body for inline co-located styles.
Questions
Should body styles participate in CSS extraction?No — they stay inline, matching the co-location intent.Should headfull component styles be scoped?Yes — auto-wrapped in@scopeduring extraction.Should theContract name — readable and deterministic.jcattribute use the contract name or a generated hash?Should bodyNo — body styles are designer-controlled.<style>inside headfull component templates also be auto-scoped?
Implementation Plan
Phase 1: Parser — skip style tags in body (don't crash)
compiler-jay-html/lib/jay-target/jay-html-compiler.ts (element target), jay-html-compiler-server.ts (server target), jay-html-compiler-hydrate.ts (hydrate target):
- When rendering child nodes, check if the node is a
<style>element - If so, emit the style content as a raw string (no template binding parsing)
- Handle in the HTML parser config —
node-html-parseralready hasblockTextElements: { script: true, style: true }for head parsing, verify this also applies to body styles
Phase 2: Emit body styles as-is
In each compiler target, when encountering a <style> element in body:
- Server element:
w('<style>'); w(styleContent); w('</style>'); - Element target:
e('style', {}, [t(styleContent)]) - Hydrate target: skip (static content, no adoption needed)
Phase 3: Auto-scope headfull component CSS
compiler-jay-html/lib/jay-target/jay-html-parser.ts — in parseHeadfullFSImports:
- When extracting CSS from a headfull component's
<head>, wrap it in@scope ([jc="contractName"]) { ... } - Add
jc="contractName"attribute to the component's root element in the injected template body - The scoped CSS merges into the page's
cssPartsas before — extraction, minification, and caching all work
Phase 4: Validation
compiler-jay-html validation or stack-cli/lib/validate.ts:
- Warn on body
<style>with bare element selectors or*selectors - Suggest
@scopeor class selectors
Phase 5: Agent-kit guide
designer/jay-html-styling.md:
- Add "Component CSS Scoping" section explaining auto-
@scopefor headfull components - Add "Inline Body Styles" section for co-located styles
- Document when to use each approach
Phase 6: Verify
yarn confirmfrom monorepo root
Trade-offs
| Choice | Pro | Con |
|---|---|---|
Auto @scope for components |
Proper encapsulation, stays in CSS pipeline | Adds jc attribute to DOM |
| Body styles as-is | Simple, matches HTML spec | No extraction/caching for inline styles |
| Class-based scoping | No @scope needed |
Synthetic classes, higher specificity |
| No scoping (current) | Simple | Style leaks between components |
| Reject body styles (error) | Clean contract | Breaks design tool output, overly restrictive |
Implementation Results
What was implemented (Phase 1, 2, partial 3)
Priority 1 + 2: Body <style> — don't crash, emit as-is
All three compiler targets now detect <style> elements and handle them as opaque nodes:
jay-html-compiler.ts— emitse('style', {}, ['...css...'])(static element)jay-html-compiler-server.ts— emitsw('<style>...css...</style>')(raw HTML)jay-html-compiler-hydrate.ts— returnsRenderFragment.empty()(skip)
extractCss — changed selector from 'head style, style' to 'head style' so body styles stay in the DOM instead of being extracted.
jc attribute — added to <jay:> tags in both parseHeadfullFSImports (compilation) and injectHeadfullFSTemplates (pre-render). This prepares for future @scope wrapping.
@keyframes / @font-face collision detection — detectCssNameCollisions function scans merged CSS for duplicate animation and font-family names, emits validation warnings.
What was deferred
@scope CSS wrapping — designed but not activated. The jc attribute is on the DOM elements, but wrapping component CSS in @scope ([jc="name"]) requires the server renderer to emit the <jay:> wrapper as an actual DOM element (with the attribute). Currently, the server element inlines the component content — the <jay:> wrapper is not preserved in the SSR output. Activating @scope requires the server element compiler to emit the wrapper element, which is a deeper change. The jc attribute is in place for when this is resolved.
Validation for broad selectors in body <style> — deferred to a follow-up.
Agent-kit documentation — deferred to a follow-up alongside @scope activation.
Tests
- 2 existing CSS extraction tests updated for unchanged behavior (no
@scopewrapping yet) - Hydration test fixtures updated via
UPDATE_FIXTURES=1forjcattribute - All 692 compiler tests pass
- All 678 hydration tests pass
yarn confirmpasses clean
Log Methodology Note
Note: These design logs are written primarily for AI agents as part of the Design Log methodology and made accessible here for human readers. The language and structure are optimized for machine consumption — expect precise, specification-style prose rather than narrative documentation.