Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[2.0 rc.13] Compiled SSR without a spread writes `style=""` / `class=""` for a nullish `style` / `class`

Open
#3,773 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
45/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
rust, typescript
Domain
backend

Research direction

Start by examining the SSR template generation in packages/babel-plugin/src/ssr/element.ts around lines 754-755 and packages/compiler/src/ssr/transform.rs around lines 2233-2241, where static style="" and class="" attributes are emitted. Compare with ssrElementAttribute in packages/web/src/server.ts around line 4637, which correctly omits nullish values. The fix should make the non-spread path skip nullish style/class values to match the spread path behavior. Update parity fixtures if they exist, and run the reproduction script from the issue to verify.

Written by the indexing model from the issue text.

Description

Describe the bug

Low priority. On an intrinsic element without a spread, a dynamic style or class that is undefined or null renders as style="" / class="", while title with the same value is omitted. With serverComponents: true both are omitted as well.

This is the template path of #3382. #3393 fixed ssrElement (the spread path) and says "Neither compiler emits these attributes; this is runtime-only". Without a spread they do: both compilers put style=" / class=" and the closing quote in the static template (element.ts#L754-L755, transform.rs#L2233-L2241), and ssrStyle / ssrClassName fill the gap with "" for a nullish value (server.ts#L4281, #L4257). Since the quotes are template bytes, changing only those two helpers can't drop the attribute.

In Chromium 153 with style-src 'self' this shows up as a style-src-attr violation (sample ""; the console offers the hash of the empty string). Chromium reports any style="" that way, a bare <div style=""> included, and class="" doesn't trip it, so the repro skips the browser. Hydration keeps the attribute: style leaves the DOM alone while hydrating (client.ts#L860).

Your Example Website or App

Self-contained Node script under Steps (published packages, no bundler).

Steps to Reproduce the Bug or Issue
  1. In an empty directory: npm i [email protected] @solidjs/[email protected] @solidjs/[email protected] @solidjs/[email protected] @babel/core
  2. Save this as repro.mjs in the same directory and run node repro.mjs:
import { writeFileSync } from "node:fs";
import { transformSync } from "@babel/core";
import { transform } from "@solidjs/compiler";
import { renderToString } from "@solidjs/web";

const source = `export const App = p => <div style={p.style} class={p.class} title={p.title} />;`;
const compilers = {
  oxc: options => transform(source, { filename: "app.jsx", ...options }).code,
  babel: options =>
    transformSync(source, {
      filename: "app.jsx",
      babelrc: false,
      configFile: false,
      plugins: [["@solidjs/babel-plugin", options]]
    }).code
};
const cases = {
  undefined: { style: undefined, class: undefined, title: undefined },
  null: { style: null, class: null, title: null },
  filled: { style: { color: "red" }, class: "a", title: "t" }
};

for (const serverComponents of [false, true]) {
  const results = {};
  for (const [name, compile] of Object.entries(compilers)) {
    const code = compile({ moduleName: "@solidjs/web", generate: "ssr", hydratable: true, serverComponents });
    const file = new URL(`app-${name}-${serverComponents}.mjs`, import.meta.url);
    writeFileSync(file, code);
    const { App } = await import(file.href);
    const template = JSON.parse(code.match(/_tmpl\$ = (\[[\s\S]*?\]);/)[1]);
    const lines = [`  template:  ${JSON.stringify(template)}`];
    for (const [label, props] of Object.entries(cases)) {
      lines.push(`  ${label.padEnd(10)} ${renderToString(() => App(props))}`);
    }
    results[name] = lines.join("\n");
  }
  const same = results.oxc === results.babel;
  console.log(`\nserverComponents: ${serverComponents} (oxc and babel ${same ? "print the same" : "differ"})`);
  console.log(same ? results.oxc : `oxc:\n${results.oxc}\nbabel:\n${results.babel}`);
}

Output:

serverComponents: false (oxc and babel print the same)
  template:  ["<div"," style=\"","\" class=\"","\"","></div>"]
  undefined  <div _hk=0 style="" class=""></div>
  null       <div _hk=0 style="" class=""></div>
  filled     <div _hk=0 style="color:red" class="a" title="t"></div>

serverComponents: true (oxc and babel print the same)
  template:  ["<div","","","","></div>"]
  undefined  <div _hk=0></div>
  null       <div _hk=0></div>
  filled     <div _hk=0 style="color:red" class="a" title="t"></div>
Expected behavior

A nullish dynamic style or class is left out of the SSR output, like title, the spread path since #3393 and a client render.

Screenshots or Videos

N/A.

Platform
  • OS: macOS 27.0
  • Browser: Chromium 153.0.8010.12 (Playwright 1.63.0), CSP check only
  • Node.js: v24.21.0
  • Version: 2.0.0-rc.13 of solid-js, @solidjs/web, @solidjs/compiler and @solidjs/babel-plugin from npm. Same output on next at e44b2e4 with a locally built Babel plugin (I didn't build the Oxc binary there). The cited files are unchanged on the current next (4738c6d).
Additional context
  • A possible fix: when the style / class expression isn't a literal (so it may be nullish), emit the whole attribute as one hole, as serverComponents does with ssrElementAttribute(key, x), which skips nullish (server.ts#L4637) and prints the same filled output above. Object literals keep their inlined path. It touches both compilers and the parity fixtures.
  • serverComponents: true isn't a workaround, since it also stops inlining object literals. A spread on the element is one: the attribute then goes through ssrElement and is left out.
  • A static class={undefined} renders class="" too, though #3382 assumed the compiler omits it (serverComponents does).
  • Not part of this: false, "" and an all-nullish object give style="" in every mode, spread and serverComponents included, per the server rule that only nullish means unset. The client also writes class="" for a dynamic empty-string class (client.ts#L777-L780).
  • inlineStyles: false (the answer in solidjs/solid-start#1996) doesn't change the SSR template.
Dominant language
TypeScript
Stars
36.1k
Forks
1.1k
Avg merge
9h 33m
Merged PRs (30d)
262

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from solidjs/solid

All issues in solidjs/solid

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.