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
Copy link
Copy Markdown

@gajus gajus commented May 4, 2026

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
Copy link
Copy Markdown

changeset-bot Bot commented May 4, 2026

⚠️ 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
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

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
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

@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
Copy link
Copy Markdown
Collaborator

astahmer commented May 6, 2026

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
Copy link
Copy Markdown
Author

gajus commented May 6, 2026

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
Copy link
Copy Markdown
Author

gajus commented May 6, 2026

@segunadebayo any comments?

@gajus
Copy link
Copy Markdown
Author

gajus commented May 7, 2026

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
Copy link
Copy Markdown
Author

gajus commented May 8, 2026

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
Copy link
Copy Markdown
Author

gajus commented May 10, 2026

@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
Copy link
Copy Markdown
Author

gajus commented May 16, 2026

@segunadebayo any update here?

@gajus
Copy link
Copy Markdown
Author

gajus commented May 19, 2026

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

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.

3 participants