Skip to content

feat: add grouped styles mode via cssMode config option#3531

Open
gajus wants to merge 7 commits into
chakra-ui:mainfrom
gajus:feat/grouped-class-mode
Open

feat: add grouped styles mode via cssMode config option#3531
gajus wants to merge 7 commits into
chakra-ui:mainfrom
gajus:feat/grouped-class-mode

Conversation

@gajus

@gajus gajus commented May 4, 2026

Copy link
Copy Markdown

Motivation

Production Panda apps routinely produce class sequences like:

class="gGTyfw cjSpVR VxsAK kYufQm iKPHnu cPHhKG bYPztT jZzPrs hYCnIA dDifmo
       hDGJaH dTBYkx bjOqtc bZRhvx fPSBzf bYPznK diIxfU jTWvec jLRTbm
       kFYoVQ fNDbsJ fNDbsm"

Atomic CSS is a good default, but at scale it has measurable costs that hash: true doesn't address (it shortens class names, not their count). This PR adds an opt-in flag to trade some CSS duplication for smaller, more inspectable HTML.

Evidence

Style recalc scales with declaration count. Per web.dev, "the worst case cost of calculating the computed element's style is the number of elements multiplied by the selector count." A Dec 2025 benchmark compared atomic vs. grouped CSS on identical visual output (5K components, 20K DOM nodes, same CSS file size). Atomic produced 35K declarations vs. 10K grouped:

Browser Atomic Grouped Delta
Chrome 169 ms 121 ms +28%
Safari 406 ms 272 ms +33%
Firefox 114 ms 82 ms +25%

Padding class names to equalize HTML size left the gap intact – declaration count was the driver.

HTML parse cost scales with bytes, even after compression (the parser sees the decompressed string). The same benchmark generated 1.1 MB of HTML for atomic vs. 0.4 MB for grouped. Peterbe's case study measured 119KB vs. 31KB HTML on the same page: parse + layout dropped from 523ms to 126ms under throttled conditions.

Dev tools UX. Acknowledged in Panda's own docs: long atomic classNames make DOM inspection painful. hash and groupedStyles solve different parts of this.

This matters most for SSR/SSG, where HTML can rarely be aggressively cached and lands on every initial render.

Tradeoff

cssMode: 'grouped' increases CSS bundle size – shared properties across css() calls no longer dedupe to single atomic classes. The right call depends on whether the bottleneck is HTML payload + recalc time or CSS bundle size. That's why it's opt-in.

What this PR does NOT change

Atomic remains the default. No breaking changes. Cascade layer ordering, hash, cva, recipes, and patterns are unaffected.

Implementation

Minimal changes localized to the extractor, behind a single config flag.

Adds opt-in `groupedStyles: true` config that groups multiple CSS properties
from a single `css()` call into one class instead of one-per-property atomic
classes. Reduces class count in HTML at the cost of potential CSS duplication.

- Encoder: `processGrouped()` hashes style objects into deduplicated groups
- Decoder: `collectGrouped()` produces single-class CSS rules via `getGroup()`
- Parser: routes `css()` and JSX style props through grouped path when enabled
- Runtime: `createCss` grouped mode reproduces encoder hashes for matching class names
- Generator: passes `grouped` flag to generated runtime context
- Full serialization support via `toJSON`/`fromJSON`
@changeset-bot

changeset-bot Bot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 195fa71

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
panda-docs Ready Ready Preview May 12, 2026 5:38pm

Request Review

@vercel

vercel Bot commented May 4, 2026

Copy link
Copy Markdown

@gajus is attempting to deploy a commit to the Chakra UI Team on Vercel.

A member of the Team first needs to authorize it.

@gajus gajus changed the title feat: add grouped styles mode via groupedStyles config option feat: add grouped styles mode via cssMode config option May 4, 2026
@astahmer

astahmer commented May 6, 2026

Copy link
Copy Markdown
Collaborator

fwiw I thought about adding it at some point but figured there were too many caveats (see the PR's description) for it to be worth adding into the core package

#1492

Tests that demonstrate where cssMode: 'grouped' breaks:
- Unresolvable values: partial extraction produces a different group hash than runtime
- Ternaries: parser splits branches into separate groups, none match the combined runtime object
- css.raw merging: build sees individual parts, runtime sees merged result

Also verifies that fully static css() and statically resolvable spreads work correctly.
@gajus

gajus commented May 6, 2026

Copy link
Copy Markdown
Author

fwiw I thought about adding it at some point but figured there were too many caveats (see the PR's description) for it to be worth adding into the core package

#1492

Thanks for sharing.

The main issues called out were ternaries and css.raw merging, where the parser splits a single css() call into multiple data entries that don't match what the runtime evaluates. These are handled now:

  • Ternaries: The build step reconstructs the combinations the runtime would see by separating base properties from branch alternatives and generating the cartesian product. css({ fontSize: "xl", color: active ? "red" : "blue" }) produces groups for both { fontSize: "xl", color: "red" } and { fontSize: "xl", color: "blue" }.
  • css.raw merging: Non-overlapping entries are merged into a single group at build time, matching what the runtime produces after Object.assign.

Everything is behind cssMode: "grouped" so it's opt-in with no impact on existing behavior.

If I missed something, would appreciate if you share a breaking test case.

@gajus

gajus commented May 6, 2026

Copy link
Copy Markdown
Author

@segunadebayo any comments?

@gajus

gajus commented May 7, 2026

Copy link
Copy Markdown
Author

Struggling to get some feedback here. Maybe @anubra266 ?

@segunadebayo

Copy link
Copy Markdown
Member

Thanks for the PR @gajus. Will have a look over the weekend and let you know

@gajus

gajus commented May 8, 2026

Copy link
Copy Markdown
Author

Thank you.

For what it is worth, I've used Claude to brainstorm edge case/test pairs, and could not identify any undesirable behaviors.

@gajus

gajus commented May 10, 2026

Copy link
Copy Markdown
Author

@segunadebayo would really like to see this land in the next version. Let me know if any further input is needed from my end.

@gajus

gajus commented May 16, 2026

Copy link
Copy Markdown
Author

@segunadebayo any update here?

@gajus

gajus commented May 19, 2026

Copy link
Copy Markdown
Author

@segunadebayo Are there any other maintainers I can tag here to accelerate the resolution?

@jfreehill

Copy link
Copy Markdown

Hello, I'm also curious to know about any movement/interest on this. The initial page-render impact potential for repeated elements with many one-off classes seems realistic.

I wonder if it would be valuable to simplify a grouping functionality by grouping rules that are already flagged as one-offs via the escape-hatch syntax (enforced via strictTokens)? In my experience so far, the most verbose class lists are those with one-offs or very specific rule sets.

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.

4 participants