Skip to content

feat(transform): fold static JSX past opaque spreads and styled() chains - #3699

Merged
segunadebayo merged 3 commits into
v2from
pr5/jsx-folds
Jul 27, 2026
Merged

feat(transform): fold static JSX past opaque spreads and styled() chains#3699
segunadebayo merged 3 commits into
v2from
pr5/jsx-folds

Conversation

@segunadebayo

Copy link
Copy Markdown
Member

Two folds that remove per-render work from JSX, plus the spread fix that unblocks them.

An element whose spreads are all style-only can be rewritten

A spread the extractor already folded into the element's styles still counted as a runtime spread, so this bailed even though nothing dynamic was left:

<styled.div {...tabBaseStyle} css={activeTabCss} />

Resolved identifier and member spreads are now recognized, and the element rewrites to a class string.

The static half of an element with {...props} is precomputed

An opaque spread used to bail the whole element, which is costly in the common wrapper shape where everything else is a module const:

<styled.button {...props} type="button" {...tabBaseStyle} css={activeTabCss} />
// →
<styled.button {...props} type="button" className={__pcx('border-style_none hover:…', props?.className)} />

The factory and the spread stay, so style-prop splitting and DOM filtering still happen for whatever the spread holds. Everything Panda can see collapses into one className.

Requires a bare identifier spread, exactly one opaque spread, no style source ahead of it, and no explicit className — the conditions under which the factory's precedence is reproducible. The class read is optional-chained, because {...props} with a nullish value is a legal no-op in JSX and props.className would throw.

A same-file styled() chain folds to its host element

const L0 = styled('button', { base: { color: 'red' } })
const L1 = styled(L0, { base: { color: 'blue' } })
export const el = <L1>hi</L1>
// →
export const el = <button className="color_blue">hi</button>

styled() renders through forwardRef, which costs a component level per instance even when the class never changes. When the chain is base-only and rooted in an intrinsic tag, the sites fold and the definition is left for the bundler to drop.

A tag folds only when it resolves to the symbol the chain declared, so a local binding that shadows the module-level one is untouched — the same check the imported-tag branch beside it already makes. Variants, an options argument, a non-local base, let/var, or a declaration inside a function all keep the runtime chain.

Tests

27 new cases in pandacss_project, roughly half of them negative — the shapes that must not fold. Two were written for the two judgement calls in this PR specifically:

  • a nullish {...props} spread still yields props?.className
  • a local const Button shadowing a module-level chain keeps its own identity and gains no class

Verified locally

Against a binary built from this branch:

  • cargo nextest run --workspace — 2288 passed
  • pnpm --filter @pandacss/compiler test — 533 passed
  • pnpm --filter sandbox-codegen test — all 11 framework scenarios
  • cargo fmt --check and clippy -D warnings clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01GVzc85Z4YGMGwQz2FnEfFo

A spread the extractor already folded into the element's styles still
counted as a runtime spread, so an element like
`<styled.div {...baseStyle} css={x} />` bailed even though nothing dynamic
was left. Recognize the resolved identifier and member spreads and let the
element rewrite.

`StyleSpread::Open` now carries the span of the spread that produced it, so
a later pass can tell which source spread is opaque rather than only that
one is.
…read

`<styled.button {...props} type="button" css={x} />` bailed entirely because
of the spread, so every style source on it was resolved per render. The
factory and the spread now stay — style-prop splitting and DOM filtering
still happen for whatever the spread turns out to hold — while every style
Panda can see collapses into one className.

Requires a bare identifier spread, exactly one opaque spread, no style
source ahead of it, and no explicit className, so the precedence the
factory gives them is reproducible. The class read is optional-chained
because a nullish spread is a legal no-op in JSX.
`const Button = styled('button', { base: … })` renders through forwardRef,
which costs a component level per instance even when the class never
changes. When the chain is base-only and rooted in an intrinsic tag, every
`<Button>` folds to `<button className="…">` and the definition is left for
the bundler to drop.

A tag folds only when it resolves to the symbol the chain declared, so a
local binding that shadows the module-level one is untouched — the same
check the imported-tag branch beside it already makes.

Variants, an options argument, a non-local base, `let`/`var`, or a
declaration inside a function all keep the runtime chain.
@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 43e68b3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@pandacss/compiler Patch
@pandacss/cli Patch
@pandacss/dev Patch
@pandacss/eslint-plugin Patch
@pandacss/language-server Patch
@pandacss/mcp Patch
@pandacss/postcss Patch
@pandacss/rollup Patch
@pandacss/transformer Patch
@pandacss/typescript-plugin Patch
@pandacss/vite Patch
@pandacss/webpack Patch
playground Patch
website Patch
@pandacss/compiler-shared Patch
@pandacss/compiler-wasm Patch
@pandacss/config Patch
@pandacss/preset-base Patch
@pandacss/preset-panda Patch
@pandacss/preset-typography Patch
@pandacss/types Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
panda-docs Error Error Jul 27, 2026 4:16pm
panda-playground Error Error Jul 27, 2026 4:16pm
panda-studio Error Error Jul 27, 2026 4:16pm

Request Review

@segunadebayo
segunadebayo merged commit 28ee00a into v2 Jul 27, 2026
10 of 13 checks passed
@segunadebayo
segunadebayo deleted the pr5/jsx-folds branch July 27, 2026 16:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant