Skip to content

& :global(...) selectors mis-compile to .jsx-HASH:scope ... (dead selector) instead of .jsx-HASH ... #94

Description

@Kegulf

DEBUGED USING CLAUDE SONNET 4.8.

Related packages

Describe the bug

When a styled-jsx rule leads with the styled-jsx scope placeholder & (e.g. & :global(.foo) or & .foo), the plugin emits an unmatchable selector and the styles silently never apply.

Internally the plugin scopes CSS with lightningcss. Per the CSS Nesting spec, lightningcss expands a top-level & into :scope. The plugin's scoping pass then prepends the generated scope class to that compound, producing:

.jsx-9fdf90a78b4c88e0:scope .foo { ... }

.jsx-HASH:scope requires a single element that is simultaneously the class .jsx-HASH and a scoping root (:scope resolves to :root/<html> in a plain stylesheet). A normal <div> never satisfies both, so every &-led rule is dead.

Expected output (what the legacy Babel styled-jsx transform produces) is the scope class replacing the &/:scope:

.jsx-9fdf90a78b4c88e0 .foo { ... }

Selectors that lead with a real class are unaffected, because no :scope is introduced:

/* source */ .wrapper :global(.foo) { ... }
/* output */ .wrapper.jsx-HASH .foo { ... }   /* ✅ correct */

So the bug is specifically: a leading & (top-level scope placeholder) → :scope → class is appended (.jsx-HASH:scope) rather than substituted (.jsx-HASH). It affects both & :global(...) and & .localClass forms. In our codebase this silently broke 91 generated selectors across ~13 components.

Root cause is in the plugin's scopeSelector: for a lone :scope compound (originating from &) it inserts the scope class before the :scope pseudo-class instead of replacing the :scope token with the scope class.

Reproduction

https://stackblitz.com/edit/vitejs-vite-j1saqhrr?file=dist%2Fvite-starter.js

Steps to reproduce

  1. Create repro.tsx:
import css from "styled-jsx/css";

export const FieldStyle = css`
  & :global(.foo) {
    display: flex;
  }
`;
  1. Build with Vite 8 (Rolldown) using the plugin:
// vite.config.ts
import { defineConfig } from "vite";
import styledJsx from "@rolldown/plugin-styled-jsx";

export default defineConfig({
  plugins: [styledJsx()],
  build: { lib: { entry: "repro.tsx", formats: ["es"] } },
});
  1. Inspect the emitted CSS string in the build output.

Actual:

.jsx-9fdf90a78b4c88e0:scope .foo { display: flex; }

→ never matches; styles do not apply at runtime.

Expected:

.jsx-9fdf90a78b4c88e0 .foo { display: flex; }

Contrast — replacing the leading & with a real class compiles correctly:

/* source: .x :global(.foo) */
.x.jsx-9fdf90a78b4c88e0 .foo { display: flex; }   ✅

System Info

@rolldown/plugin-styled-jsx 0.1.1 (latest published; also affects 0.1.0)
lightningcss (plugin dep) 1.32.0
vite 8.0.16 (Rolldown + Oxc)
@vitejs/plugin-react 6.0.2
styled-jsx 5.1.7
Node 24.10.0
OS Windows

Used Package Manager

npm

Validations

Metadata

Metadata

Assignees

No one assigned

    Type

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions