Skip to content

css: write the vendor prefixes for user-select, appearance, hyphens and five more properties - #45814

Open
robobun wants to merge 8 commits into
mainfrom
robobun/e36ea53a/css-prefix-handler
Open

robobun wants to merge 8 commits into
mainfrom
robobun/e36ea53a/css-prefix-handler

Conversation

@robobun

@robobun robobun commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Question for a maintainer: keep a prefix that the author wrote (this PR, autoprefixer, esbuild), or replace it (lightningcss)? Keeping needs state across the !important list. Details in Notes.

Problem

  • bun build writes no vendor prefix for user-select, appearance, hyphens and five more properties under its default browsers. The docs say prefixing is on.
  • The lightningcss PrefixHandler was never ported (src/css/properties/prefix_handler.rs). Port backlog (MVP of CSS parser #14167), no user report. css: consolidate the open CSS parser fixes #47025 counted 1,116 real stylesheets that lack a needed prefix.

Fix

  • Add PrefixHandler as the 16th entry of the handler chain, as in lightningcss, and the nine rows it needs. It keeps written prefixes, merges only equal values, and also prefixes var() and initial.
  • print-color-adjust gets no -moz- form. css: consolidate the open CSS parser fixes #47025 landed the two other changes that the rows need.
  • Verified: css.test.ts (124 new cases, 56 fail before), vendor-prefix.test.ts, the other css suites.
  • Self-reviewed: 69 concerns raised, 62 addressed. Notes list the seven not taken.

Background

  • src/css/prefixes.rs says which browsers need which prefix. A typed property stores a value and its prefixes, and prints one line for each.
  • Considered a name-keyed prefix step with no rows: it reaches more names, but rows also give transition lists prefixes.

Downsides

  • Safari applies what it ignored: * { user-select: none } ships -webkit-user-select: none.
  • Measured before css: consolidate the open CSS parser fixes #47025: an unclaimed declaration takes 66 more minify instructions (863 to 929). .text grows by 18,432 bytes.
  • CSS modules: bun build prints a composes conflict error for these names in two more cases. The exit code stays 0.
Notes

After #47025. This branch has main at 7942008. #47025 lists this PR with #40368 and #40369 (default browsers, cssTarget) as needing a decision. It landed two changes that this PR carried: the duplicate-rule compare of names and importance (from #33684) and the prefix print of @import … supports() (from #38589). So this PR no longer touches src/css/rules/style.rs and src/css/rules/supports.rs, and it is 14 files. #47025 did not take #42042 (all), #45747 (owner dispatch), #46206 (origin rows) and #38692 (transition list). Where a note below names one of them, that change is not coming.

The question. For -webkit-text-size-adjust: 100%; text-size-adjust: 100% under the default browsers, lightningcss 1.32.0 prints -moz-text-size-adjust: 100%; text-size-adjust: 100%: the written -webkit- line is gone, and the default list has no iOS Safari, which needs it. Bun's flex, transform, transition, border-radius and box-shadow handlers follow lightningcss. This handler keeps the prefixes that the author wrote. It can remove a dead rule that main prints. An equal prefixed and plain pair is now one declaration. The duplicate check then matches an earlier rule with the same names. Under the default browsers .btn{user-select:none} .q{top:0} .btn{-webkit-user-select:text;user-select:text} loses its first rule, which the last rule overrides. Main prints all three. The rule of this PR is the keep rule of esbuild for a whole block, with one exception: after all, Bun adds the copy again, as autoprefixer and lightningcss do, also across the !important list. The keep rule needed state across the two importance lists twice in review (e0fc9b1, f52e2a1). If you want the lightningcss rule here, PrefixHandler loses written, important and the two see_*_list calls in DeclarationBlock::minify, handle_property takes the prefixes of the targets alone, and the cases that pin a kept prefix get new expected output.

Where the handler differs from lightningcss 1.32.0, default browsers.

Input lightningcss 1.32.0 This PR
-webkit-text-size-adjust: 100%; text-size-adjust: 100% -moz-text-size-adjust: 100%; text-size-adjust: 100% keeps -webkit-text-size-adjust too
-webkit-user-select: text; user-select: none adds -webkit-user-select: none after the written line (parcel-bundler/lightningcss#977) as written
-webkit-user-select: none; cursor: pointer; user-select: none moves user-select: none in front of cursor as written
appearance: none; appearance: base-select only base-select, three times both values
appearance: none; -moz-appearance: textfield textfield for every prefix none for every prefix, then -moz-appearance: textfield
user-select: initial, user-select: var(--x) no prefix -webkit- copy
print-color-adjust: exact also -moz-print-color-adjust: exact no -moz- line

The rules behind the rows:

  • A prefix that the author wrote stays. A prefix that the block already declares for the property is not added again, so a written -webkit- value stays the value for Safari.
  • A declaration merges into the previous one of the same property only when the value is equal and no unknown name is between them. An unknown name can be one more spelling of the property (-khtml-user-select). all resets everything.
  • The rule is for one rule as the author wrote it. .a{-webkit-user-select:none} .a{user-select:text} are two rules, so the second one gets its -webkit- copy, also after Bun merges them. autoprefixer, esbuild and lightningcss print the same for two rules. One exception: a merged rule is one rule for all. .a{-webkit-user-select:text;user-select:none!important} .a{all:unset} gets -webkit-user-select:none!important when the two rules are adjacent, and not when another rule is between them. Over 1,764 pairs of rules with all and !important, 28 differ in this way. In each of the 28 the merged rule gives the browser that reads only the prefixed name the value that the other browsers get.
  • The !important declarations of a rule are a list of their own. A prefix that the author wrote in one list is not added in the other: -webkit-user-select: text; user-select: none !important stays as written. all resets a written prefix here too. -webkit-user-select: text; all: unset; user-select: none !important gets -webkit-user-select: none !important, as autoprefixer 10.6.1 and lightningcss 1.32.0 print it. esbuild 0.28.2 prints that block as written. An all !important also resets what the normal list wrote. Every declaration named all counts, also one with an invalid value such as all: none, which a browser drops.
  • A value that did not parse gets the same prefixes. Without that, .a{user-select:none} .b{user-select:initial} gives Safari the none and not the reset.
  • Firefox 48 to 96 reads color-adjust, and no Firefox reads -moz-print-color-adjust. The table entry in src/css/prefixes.rs is corrected by hand, with the same condition in build-prefixes.js. This PR does not write color-adjust.

Three tests in css.test.ts check the declaration that three kinds of browser apply: one that reads only user-select, one that reads both names, and one that reads only -webkit-user-select. They share one oracle, changed_winners in test/js/bun/css/util.ts. For the last kind of browser it says: a written prefixed declaration that wins in a rule stays the winner, and without one the browser gets the declaration that wins for the unprefixed name. "the prefixes change no winning declaration" runs every block of up to four declarations over six declarations (1,554 blocks). "!important and all change no winning declaration" runs every block of up to three over eight declarations (584 blocks). "two rules of one selector keep the winning declaration" runs 468 pairs of rules with and without !important.

Repro. bun build of a sheet with backdrop-filter, user-select, appearance, hyphens, mask-image, backface-visibility, box-decoration-break and print-color-adjust goes from 0 of 8 prefixed to 6 of 8. mask-image needs the mask handler. backdrop-filter is #44648. The five more properties of the first bullet are backface-visibility, box-decoration-break, print-color-adjust, tab-size and text-size-adjust. Bun's own sheets have the gap: src/runtime/bake/client/overlay.css and the bun init React templates write bare user-select: none and appearance: none.

Against lightningcss 1.32.0. Measured on the base before #47025. 306 one-declaration sheets cover every place where lightningcss can write a prefix.

Browsers lightningcss writes a prefix Bun writes the same, before after
default (chrome 80, edge 80, firefox 78, safari 14, opera 67) 89 39 52
about 2013 (chrome 25, firefox 20, safari 7, ie 10, ...) 195 115 132
oldest (chrome 4, firefox 2, safari 3.1, ie 8, ...) 195 112 128

The 37 default places that still differ: mask* and mask-border* (18), text-decoration and text-emphasis* (7), backdrop-filter (4, #44648), resolution media features (4), image-set() in list-style-image, :autofill, @supports (user-select: none), and the -moz-print-color-adjust line above. #47025 has since added the :autofill prefix.

Fixture sheets. The 15 sheets in test/js/bun/css/files, target browser, against main at 7942008: 6 outputs change. 58 prefixed declarations added (-webkit-appearance 18, -moz-appearance 18, -webkit-user-select 16, -webkit-backface-visibility 4, -moz-text-size-adjust 2), 0 removed, output 3,067,457 to 3,069,093 bytes. Bun keeps 40 written declarations that lightningcss removes there (-moz-user-select 14, -webkit-box-sizing 14, -ms-user-select 9, three others).

--target=bun output changes in 1 of 15: in one rule of pico.css, appearance: none joins its prefixed copies in front of display: inline-block. lightningcss does the same.

#47025 measured the gap on 10,116 real stylesheets: 1,116 lack a prefix that they need at the default targets.

Duplicate rules. With typed rows and merged prefix pairs, more rules reach the duplicate-rule check: .a{-webkit-user-select:none!important;user-select:none!important} .b{top:0} .a{user-select:text} must keep its first rule. The check has to compare names and each importance list. #47025 landed that, and the cases in css.test.ts stay as pins.

@import … supports(). #47025 landed the print of the written prefix. With typed rows, supports(-webkit-user-select: none) prints ((-webkit-user-select: none)). @supports (user-select: none) itself gets no prefixed alternative: a (property: value) group does not parse as a declaration on main, and the lightningcss step SupportsCondition::set_prefixes_for_targets is not ported. For the same reason a group of an @supports rule or of an and/or chain is raw text and prints as written, also for the names that now have a row. Six conditions in css.test.ts and two imports in vendor-prefix.test.ts pin the written names.

Second pass. Bun minifies a merged rule again. A rule that is minified twice prints the same. A merged rule can get one more copy after an all (the exception above). transition: user-select 1s now gets -webkit-user-select 1s in front, and when its rule is merged with a neighbour the prefixed entry is there twice. That is an existing bug of the transition handler (main prints -webkit-background-clip 1s twice for .a{transition:background-clip 1s} .a{color:red}). #38692 had the fix, and #47025 did not take it.

CSS modules. composes from another file reports "The value of user-select in the class foo is undefined" in two new cases: one class sets -webkit-user-select and the other user-select, and a class whose second rule (for example in @media) does not set the property. Both are how properties with a row already behave (-webkit-transition and transition). bun build prints the error and still exits 0, as for every composes conflict on main.

Cost. Measured on the base before #47025, with the method of #45747: qemu-x86_64 -d exec trace of StyleSheet::minify on release builds of main (5866b5f) and this branch at 09c03b1, one rule with 5,000 and 10,000 declarations. The two later !important commits (e0fc9b1, f52e2a1) are not in these numbers. They add one check for each rule that the !important list is empty, and one pass over each list of a rule that has !important declarations.

Declaration Instructions before after
zoom: 1 863.1 929.1
--x: 1 863.1 923.1
display: block 928.2 995.2
box-sizing: border-box 938.1 1,007.0
user-select: none 863.1 1,021.0
appearance: none 863.2 1,033.0
color: red 850.0 861.0
margin: 0 823.0 824.0
background: red 981.0 983.0
Fixture sheet, minify pass Before After
animate.css 665,894 690,690
tailwind.css 5,836,687 5,921,142
bootstrap-4.css 9,947,681 10,052,579
bulma.css 27,655,465 28,207,584

The handler is the last of 16 chain entries, so a declaration that no handler claims pays for one more call. A whole bun build <sheet> runs 0.2 to 0.7% more user instructions (perf_event_open, 40 interleaved pairs, six fixture sheets), and the wall-clock difference is inside the noise. Allocator calls for each declaration are 0 before and after. 4,000 merged rules cost 2.0 times what 2,000 cost.

Sizes. size -A, also at 09c03b1: .text 81,071,045 to 81,089,477, .rodata 22,532,844 to 22,536,940. DeclarationHandler 6,848 to 7,000 bytes (two for each stylesheet). PropertyBitset 32 to 64 bytes: 263 tags no longer fit in 256 bits. Property stays 216 bytes, PropertyId 24.

all with !important (f52e2a1). Found in review, with no user report. The head before it printed these blocks as main and 1.4.3 do. The 15 fixture sheets give the same 60 outputs before and after (two targets, plain and minified).

Other open PRs. #40369 raises the default browsers and #40368 adds a cssTarget option. Both are open. With the list of #40369, appearance, backface-visibility and tab-size need no prefix. The bundler test and the docs example only use properties that need one under both lists. #44648 (open) adds backdrop-filter. Three things that earlier notes planned for a second lander do not apply, because #47025 did not take the other PR: is_all keeps its arm for the unknown name all (#42042), the two see_*_list calls need no owner mark (#45747), and transform-origin and perspective-origin stay out of define_prefixes! until their rows parse a valid value (#46206).

Not taken from the self-review.

  • A macro for new rows. It changes how every later row is written, so it is its own change.
  • A test that each row parses its own initial value. Four rows fail it on main (transform-origin, perspective-origin, mask-position, all). That fix is separate work, and this handler leaves the first two out until they parse.
  • A separate commit for the merge rule. The rule is the question at the top.
  • Prefixes in @supports conditions. They need a (property: value) group that parses as a declaration first.
  • A narrower all in f52e2a1: only a CSS-wide keyword or a value with var() resets. That needs a row for all, which main does not have.
  • To hold f52e2a1 until the question at the top has an answer. The copy after all is what lightningcss prints too, so it stays under both answers.
  • A mark on merged rules so that the second pass adds no copy. Two cases pin what the second pass does.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/cli.test.ts

…nd five more properties

Port the lightningcss `PrefixHandler` as the 16th entry of the handler
chain, with typed rows for the nine properties it needs: tab-size,
hyphens, text-align-last, text-decoration-skip-ink, text-size-adjust,
box-decoration-break, user-select, appearance and print-color-adjust.

Unlike lightningcss, the handler keeps a prefix that the author wrote,
does not add a prefix that the block already declares, merges only equal
values, and also prefixes a value that did not parse (`var()`, `initial`).

The new rows need three more changes: the `@import ... supports()`
condition prints the prefix it is written with, duplicate-rule detection
compares the names of unknown properties, and the prefix table no longer
gives `print-color-adjust` a `-moz-` form that no Firefox reads.
@robobun

robobun commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 4:42 PM PT - Oct 27th, 2026

✅ @robobun, your commit 6de8f2836400febefa4419faf5f47a7599407c5c passed in Build #133985! 🎉


🧪   To try this PR locally:

bunx bun-pr 45814

That installs a local version of the PR into your bun-45814 executable, so you can run:

bun-45814 --bun

@robobun

robobun commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator Author

Reproduced on 1.4.3 and on main with bun build:

.modal { user-select: none; }
.button { appearance: none; }
.title { hyphens: auto; }
.card { backface-visibility: hidden; box-decoration-break: clone; }
@media print { .page { print-color-adjust: exact; } }

The output has no -webkit- line. lightningcss 1.32.0 with the same browsers (Chrome 80, Edge 80, Firefox 78, Safari 14, Opera 67) writes one for each declaration. With this branch bun build writes them too.

  • bun bd test test/js/bun/css/css.test.ts: 124 new cases. 56 of them fail with main's src/css (7942008) and all pass with this branch.
  • test/bundler/css/vendor-prefix.test.ts shows the same through the bundler: 2 of its 3 cases fail on main.
  • bun bd test test/js/bun/css/ test/bundler/css/ test/bundler/esbuild/css.test.ts and the composes cases of test/bundler/cli.test.ts pass on the debug build. The one exception is css-fuzz.test.ts: CI skips it, and its 1,000 builds do not fit its 10 second limit on a debug build.
  • The branch has main at 7942008, which includes css: consolidate the open CSS parser fixes #47025. That PR lists this one as needing a decision.

The one open point is the question at the top of the description: keep a written prefix, or replace it as lightningcss does.

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 18664daa-442a-440a-a35f-2caa3d3cb107
📥 Commits

Reviewing files that changed from the base of the PR and between e03f9c5 and 427318b.

📒 Files selected for processing: 5
  • src/css/declaration.rs
  • src/css/properties/prefix_handler.rs
  • src/css/rules/style.rs
  • test/bundler/css/vendor-prefix.test.ts
  • test/js/bun/css/css.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

Walkthrough

The change adds CSS property models and target-based vendor-prefix processing. It integrates prefix handling into declaration processing, updates bundler behavior for prefixed declarations and @supports, and adds tests and documentation.

Changes

CSS Vendor Prefixing

Layer / File(s) Summary
CSS property contracts
src/css/properties/text.rs, src/css/properties/ui.rs, src/css/properties/properties_generated.rs, src/css/generics.rs
Adds text and UI property types and registers them for parsing, serialization, cloning, equality, and prefix metadata.
Target-based prefix processing
src/css/properties/prefix_handler.rs, src/css/declaration.rs, src/css/prefixes.rs, src/css/build-prefixes.js
Adds declaration-level prefix handling. It combines authored and target-required prefixes, merges matching declarations or appends clones, and omits the Mozilla prefix rule for print-color-adjust.
Bundler integration and validation
src/css/rules/*, test/bundler/cli.test.ts, test/bundler/css/vendor-prefix.test.ts, test/js/bun/css/*, docs/bundler/css.mdx
Updates property comparison and @supports serialization. Adds tests for prefix output, preservation, browser targets, declaration ordering, and repeated rules. Documents supported prefix behavior.

Suggested reviewers: jarred-sumner

Priority: Normal

Merge Risk: Minimal · up to 42731

The reviewed prefix behavior preserves author-written declarations, and no merge-blocking issue remains after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: adding vendor prefixes for user-select, appearance, hyphens, and five additional properties.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is complete and relevant. It clearly explains the problem, implementation, scope, tradeoffs, and verification results. It does not use the template headings exactly, but it includes eq…
Full details: Description check

Explanation

The description is complete and relevant. It clearly explains the problem, implementation, scope, tradeoffs, and verification results. It does not use the template headings exactly, but it includes equivalent information, including how the changes were verified.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the index bookkeeping in PrefixHandler (last[slot], start, floor) for out-of-bounds access: dest only grows during a block's handle_property calls (no handler shrinks it), last[slot] is only read when start is set, and finalize resets the state between blocks, so no panic path. The same_property change in src/css/rules/style.rs only adds a name comparison on top of the existing PropertyId equality, so it can only reject merges the base branch allowed, never introduce new ones.

Extended reasoning...

The PR adds a PrefixHandler to the CSS minifier handler chain and nine new typed property rows in src/css/, plus merge and @ supports printing changes that affect all prefixed properties; it touches no auth, crypto, or injection surface. Four verified findings are posted inline (rule-merge re-minify divergence for Safari, a CSS-modules composes regression, a docs claim about print-color-adjust on Firefox, and a missing exit-code check in cli.test.ts), so a human review is already signalled; this note records only the two additional concerns examined and ruled out.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/css/properties/prefix_handler.rs
Comment thread src/css/properties/properties_generated.rs
Comment thread docs/bundler/css.mdx
Comment thread test/bundler/cli.test.ts
Comment thread src/css/properties/prefix_handler.rs Outdated
Comment thread src/css/rules/style.rs Outdated
The `!important` declarations of a block have their own handler. Each
`PrefixHandler` now gets the prefixes that the author wrote in the other
list, so `-webkit-user-select: text; user-select: none !important` gets
no generated `-webkit-` copy.

`StyleRule::is_duplicate` compared the combined length of the two lists
and only the shorter part of each. A rule with `!important` declarations
was a duplicate of a later rule with the same properties and no
`!important`. It now compares each list in full.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest push addresses the two threads from the last round: see_other_list seeds each handler with the other list's written prefixes (pinned by the !important cases in css.test.ts), and is_duplicate now compares both declaration lists length-by-length. Beyond the inline findings, I also checked that close() on all keeps elsewhere while resetting written — correct, since the other list's declarations are not reset by all in this one — and that the see_other_list guard on an empty important list loses nothing, because an empty list seeds nothing either way.

Extended reasoning...

Third review of this PR; the two confirmed inline findings (prefix stripped from later @ supports conditions, and a std eq_ignore_ascii_case where the crate uses the in-tree helper) remain for the author. The prior-round objections on the important-list prefix and the vacuous duplicate-rule compare are addressed in commit e0fc9b1 with tests. No security-sensitive surface; the PR still carries an open maintainer design question on keeping versus replacing author-written prefixes.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🔴 src/css/rules/supports.rs — Safari and Chrome 80-83 users lose any @ supports block whose second or later condition names a prefixed property such as -webkit-user-select or -webkit-appearance. At src/css/rules/supports.rs:287 the id is rewritten with with_prefix(NONE), and that stripped id is what :292 adds and :299 stores, so (display: grid) and (-webkit-user-select: none) prints (user-select: none), which no Safari supports. On the base branch these names were unknown and kept their spelling. Fix: key the dedup on the stripped id but add and store the original property_id_ prefix for every later condition, in and/or chains and @ import … supports() alike.

    Why this was flagged

    Input @ supports (display: grid) and (-webkit-user-select: none) { .x { color: red } } built with default targets. SupportsCondition::parse (src/css/rules/supports.rs:234-307) keeps the first condition as in_parens with its prefix, but for every later Declaration it computes property_id = property_id_.with_prefix(NONE) at :287, then either calls d.property_id.add_prefix(property_id.prefix()) at :292 (adds NONE, not WEBKIT) or pushes Declaration { property_id, value } at :296-299 with the stripped id. The diff's new row makes -webkit-user-select parse to PropertyId::UserSelect(WEBKIT) (properties_generated.rs:1601) and the new printer at :205 prints exactly the stored flags, so the output is @ supports ((display: grid) and (user-select: none)). Safari (every version) and Chrome 80-83 do not support the unprefixed name, so the block stops applying there. On the base branch -webkit-user-select was PropertyId::Custom("-webkit-user-select"), with_prefix was a no-op, and the output kept (-webkit-user-select: none). No test covers a prefixed name in a non-first @ supports condition.

    Verification: src/css/rules/supports.rs line 287 let property_id = property_id_.with_prefix(css::VendorPrefix::NONE); replaces the prefix slot; line 292 d.property_id.add_prefix(property_id.prefix()) uses the stripped id; line 296-299 pushes Declaration { property_id, value } with the stripped id, so -webkit-user-select: none as a second condition prints as (user-select: none).

Comment thread src/css/properties/prefix_handler.rs Outdated
…II compare

A `(property: value)` group in an `@supports` rule or in an `and`/`or`
chain is raw text, so it prints as written. The new cases pin that for
the names that now have a typed row.

`PrefixHandler::see_other` compares `all` with
`strings::eql_case_insensitive_ascii_check_length`, like the rest of
`src/css`.
@robobun

robobun commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

On the @supports finding of the last review (src/css/rules/supports.rs:287): it does not reproduce. The fold never sees a declaration.

  • parse_in_parens (supports.rs:336) tries a nested condition and then falls back to Unknown. It does not call parse_declaration. So a (property: value) group of an @supports rule, and each group of an and or or chain, is raw text on main and on this branch.
  • parse_declaration has one caller, @import … supports(property: value) (css_parser.rs:1194). There the declaration is the whole condition, and it does not pass through the fold.

Output of this branch under the default browsers:

/* in */
@supports (display: grid) and (-webkit-user-select: none) { .x { color: red } }
@import "./c.css" supports((display: grid) and (-webkit-user-select: none));

/* out */
@supports (display: grid) and (-webkit-user-select: none) { .x { color: red; } }
@supports (display: grid) and (-webkit-user-select: none) { .c { color: red; } }

427318b adds these cases: six conditions in css.test.ts and two imports in vendor-prefix.test.ts. A change that makes the fold reachable then has to keep the prefix. #38589 is that change. It adds the parse_declaration fallback and corrects the fold in the same diff. This PR does not change the fold, because no input reaches it and no test here can cover it.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the two inline nits, I re-checked the points raised on earlier pushes against the current code: StyleRule::is_duplicate (src/css/rules/style.rs) now requires equal lengths for the normal and !important lists separately before the zip, and the all-important-vs-normal case is pinned at test/js/bun/css/css.test.ts:8603; DeclarationBlock::minify (src/css/declaration.rs) seeds each list's PrefixHandler with the other list's written prefixes; and the all compare in prefix_handler.rs uses the in-tree strings::eql_case_insensitive_ascii_check_length. The new handler still diverges from lightningcss on purpose (written prefixes kept, no re-add), so a maintainer should weigh that policy question the author asks in the description.

Extended reasoning...

The change adds a PrefixHandler to the CSS declaration handler chain, nine new typed prefixed Property variants, a PrintColorAdjust split in prefixes.rs, a rewritten is_duplicate, and a supports() serialization fix, with ~1300 lines of tests; it touches no auth, crypto or injection surface. Earlier red findings (cross-list !important prefixing, vacuous is_duplicate zip) are addressed in commits e0fc9b1 and 427318b as verified from the code, leaving only the two inline nits. The PR is large, intentionally diverges from the lightningcss reference in merge semantics, and the author explicitly asks a maintainer to decide the keep-vs-replace prefix policy, so a human decision is still needed.

Comment thread src/css/properties/prefix_handler.rs Outdated
Comment thread src/css/properties/prefix_handler.rs
Found in review, not reported by a user. The previous head printed these
blocks as main does.

The handler of the `!important` list adds no copy for a prefixed name that
the normal list of the block writes. That ignored `all`. In
`-webkit-user-select: text; all: unset; user-select: none !important` the
written prefixed value is reset, so a browser that reads only
`-webkit-user-select` ended on `unset` and the others on `none`.

Now that handler takes only the prefixes that the normal list writes after
its last `all`, and drops them at an `all !important`, which beats every
normal declaration. The handler of the normal list still takes every prefix
that the `!important` list writes: that declaration, or an `all !important`
after it, beats a normal copy.

Two adjacent rules of one selector are merged and minified again. The merged
rule is one rule for `all`, so it can get a copy that the two rules do not
get when another rule is between them. Two cases pin that.

The three winner tests share one oracle in `util.ts`. The new test runs 584
blocks with `!important` and `all`. 12 of them are wrong on the previous head.

Every declaration named `all` counts as a reset, also one with an invalid
value.

The comment on the handler list said that `transform-origin` and
`perspective-origin` have no typed value. They have a row, but no valid
value parses into it.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

Jarred-Sumner added a commit that referenced this pull request Oct 27, 2026
### What does this PR do?

Consolidates the open pull requests on the CSS parser (`src/css`:
parsing, minifying, lowering, printing, CSS modules) into one.

179 open pull requests were in scope. Each was judged by one rule: **the
output must mean to a browser what the input meant.** Every claim was
run through a build of `main`, a build with the change, lightningcss
1.33 and Chromium 156 (CSSOM, computed values, painted pixels for colors
and gradients), and counted against 10,116 real stylesheets: the
published CSS of about 190 popular npm packages, plus stylesheets from
open source repositories. 107 land here, whole or in part, most of them
cut down. The source diff is net negative.

Of the 10,116, 9,509 are stylesheets that someone ships. The rest are
test fixtures of other tools (521), Tailwind v4 source that needs its
plugin (59) and SCSS, PostCSS or template source (27).

| 9,509 shipped stylesheets | `main` | this branch |
|---|---|---|
| fail to build | 71 | 35 |
| ... of those, valid CSS that Bun rejects | | 0 |
| minified output reads differently in Chromium than the input | 552 | 2
|
| CSS modules with a name that is referenced but not defined, or
exported but not defined | 166 | 0 |

| all 10,116 | `main` | this branch |
|---|---|---|
| fail to build | 192 | 141 |
| minified output reads differently in Chromium than the input | 593 |
15 |
| build on `main`, fail here | | 0 |
| right on `main`, wrong here (minified, and at the default targets) | |
0 |
| size of the minified output, no targets | 162.32 MB | 162.47 MB
(+0.093%) |
| size of the output at the default targets | 224.1 MB | 210.2 MB
(-6.2%) |

A syntax error is a build error, with one exception: the IE star hack
`*zoom: 1` is skipped, because tachyons and handsontable ship it. The 35
shipped stylesheets that fail are invalid CSS: 28 have a selector that
Chromium drops (semantic-ui-css, swiper, @ionic/core, Adobe Spectrum,
xp.css), 2 an `@import` after other rules, and one each `@supports not X
and Y`, a nested `@keyframes`, a stray comma, a selector list in
`::slotted()`, `@media screen\0` (govuk-frontend). The 2 that read
differently fold a `color-mix()` inside a gradient (see the known gaps).

The growth of the minified output is rules and declarations that `main`
deleted by mistake: 87 KB of rules, 45 KB of prefixed declarations that
only browser targets can rule out.

#### Popular CSS that `main` breaks today

- normalize.css, modern-normalize, sanitize.css: `font-family:
monospace, monospace` is collapsed to `monospace`, which changes the
font size from 16px to 13px (#46662).
- CKEditor 5, Tabler, Carbon, VitePress, Ionic, PatternFly, Excalidraw:
a rule of custom properties is deleted as a "duplicate" of a later rule
with the same selector and other custom properties. 37 stylesheets lose
1,681 custom properties (#33684).
- Material Components, daisyUI, DocSearch, plyr: a `var()` size loses to
its own fallback (#42101).
- nextra, daisyUI, CKEditor: declarations written after a nested rule
are moved above it (#45462).
- antd 4, purecss, govuk-frontend: browser hacks in selector lists start
to apply everywhere (#42100).
- foundation-sites, flatpickr, 98.css, blueprintjs, scalar,
react-datepicker: the build fails on valid CSS (#46756, #38552, #38534).
tachyons, handsontable: it fails on the IE star hack (#46751).
- nodejs.org, Mantine: a CSS module exports classes that no rule
defines, because the `@apply` or `@mixin` in them is moved out of the
rule (#46759).
- hint.css and 42 more: `z-index: 1000001` and `1000000` become the same
number (#46715).
- Adobe Spectrum: `--version: 5.0.1` prints as `5.1`.
- A quarter of all stylesheets: minified output has a stray space in
`align-items: center` (#41993).

#### Behavior changes to look at first

1. **`light-dark()` is kept as written at the default browser targets**
(#42009). The lowered form `var(--buncss-light, a) var(--buncss-dark,
b)` is invalid unless a compiled `color-scheme` covers the element: 184
of the 215 real stylesheets that get it never define the variable, so
every browser drops the declaration. The docs section on the polyfill is
removed.
2. **`border` is not built from longhands** (#42072):
`border-width:0;border-style:solid;border-color:red` stays three
declarations, because `border` also resets `border-image`. 305 real
stylesheets grow by 30 KB in total; `bootstrap.min.css` by 113 bytes, 5
gzipped. No real stylesheet was found where the old merge shows, so this
is the one to drop if size matters more. 14 existing expectations
change.
3. **A CSS module no longer exports names that only occur inside
`:global(...)`** (#45487). 73 real modules lose 202 export keys, all of
them names that no rule has. `import { g }` or `composes: g` of such a
name becomes a build error.
4. **Container names and `@counter-style` names stay global in a CSS
module** (#45247 by @mikamikasuki). `main` hashes the `@container` name
but not `container-name`, so a named query never matches.
postcss-modules (Vite, css-loader, Next.js) keeps them global;
lightningcss and esbuild hash both. Global keeps a container that is
queried from another file or from JS working.
5. **Syntax errors are build errors, also two that `main` lets
through**: a media query that stops short (`@media screen and {`,
`@media not {`, `@import "a.css" screen and;`) and `@media (width <) foo
{`. No stylesheet of the 10,116 has either. A condition that is valid by
the grammar though no browser knows it, `( ... )` or `fn( ... )`, is
kept as written (#46756): `(min-width:0\0)`, `(not(hover))`.
6. **Declarations after a nested rule stay where they were written**, as
a nested declarations rule (#45462). Changes the output of 199 real
stylesheets; in a fuzz of 2,850 nested programs compared by computed
style, `main` was wrong in 1,056 and the pull request's code, before it
was trimmed here, in none.
7. **`Bun.color`**: `color()` inputs are converted instead of returned
as a string (`Bun.color("color(srgb 1 0 0)", "hex")` was `"color(srgb 1
0 0)"`); `currentColor`, system colors and `light-dark()` give `null` in
the converting formats instead of the keyword as a string; a relative
color or `color-mix()` outside sRGB is clipped, not gamut-mapped
(`hsl(from oklch(100% .399 336.3) h s l)` is `#ff5bff`, was `#fff`).
8. The `:-moz-any()` and `:-webkit-any()` copies of rules with `:is()`
are gone at the default targets (#45945, #42029). The `:-webkit-any()`
copy outranked the original rule: `.title{color:blue}
:is(h1,h2,h3){color:red}` painted red. 5.8% of all default-target output
was the `:-moz-any()` copies.
9. `Property` grows from 216 to 224 bytes: `Url` stores its text as a
slice so that two `url()` values can be compared (#46895). The pull
request packed a pointer and a `u32` behind `unsafe` to avoid that.

#### Not in this PR, needs a decision

- **Raise the default browser targets (#40369) and add a `cssTarget`
option (#40368).** Today: Chrome 80, Edge 80, Firefox 78, Safari 14,
Opera 67, and no public way to change them. At Vite 8's floor the output
of the 9,924 stylesheets is 15.3% smaller (3.9% gzipped). About a dozen
fixes here only matter at today's floor, and code for older targets is
unreachable without the option.
- Vendor prefixes for `user-select`, `appearance`, `hyphens` and five
more (#45814): 1,116 real stylesheets lack a prefix they need at the
default targets. And for `backdrop-filter` (#44648): 173.
- Clip instead of gamut-map in the sRGB fallback of a plain wide-gamut
color: deletes about 90 lines, changes every pinned fallback hex. If the
mapping stays, #44647 by @oddharsh makes it closer to what browsers
paint (colors off by 2/255 or more: 168 to 110 of 3,170 real ones).

#### Changes

**Rules: duplicates, merging, nesting, fallbacks**

- The `:-moz-any()` copy of every rule with `:is()` or `:not(a, b)` is
gone: Firefox 78 has `:is()` (#45945). 1,164 real stylesheets
- Keep declarations written after a nested rule in place with a nested
declarations rule (#45462). 379 real stylesheets
- With nesting compiled away, an unknown at-rule inside a style rule was
moved out of it: `.wrapper{@apply flex}` printed a bare `@apply flex;`
and the rule was gone, so a CSS module exported a class that no rule
defines. It now stays in a block under the compiled selector. 72 real
modules with Tailwind's `@apply`, 25 with Mantine's `@mixin` (#46759).
316 real stylesheets
- Keep !important in the rules generated for logical properties and
color-scheme (#38597). 311 real stylesheets
- With browser targets, remove an earlier duplicate rule only when its
declaration block is equal (#46861). 171 real stylesheets
- Compare custom property names in `PropertyId` equality and importance
in duplicate-rule detection (#33684). 40 real stylesheets
- Keep same-condition @supports rules and empty named @layer blocks
(#46733). 31 real stylesheets
- Keep target-added vendor prefix passes on cloned and re-minified
rules, and never print a bare `:full-screen` (#43339). 12 real
stylesheets
- Keep the selectors on rules added for color fallbacks and logical
properties (#43240). 3 real stylesheets
- A later declaration of a property drops the `@supports` color copies
of the earlier one, which printed after the rule and won (#45560). 2
real stylesheets
- Do not merge style rules in a stylesheet that holds `revert-rule`
(#46953). 1 real stylesheets
- `@import ... supports(-webkit-mask: none)` kept its prefix (#38589)

**Properties and shorthands**

- Print align-items:center without a stray space when no safe/unsafe
keyword is given (#41993). 3,317 real stylesheets
- Emit the physical longhands instead of the inset shorthand for targets
without it (#38583). 1,452 real stylesheets
- Keep the stops of an upward gradient as written, and drop a `50%` hint
only at the midpoint (#46790). 104 real stylesheets
- Emit unparsed border-block-* declarations when compiling logical
borders (#38582). 89 real stylesheets
- Keep repeated font families and dedupe only the inserted system-ui
fallbacks (#46662). 88 real stylesheets
- A `width` / `height` / `min-*` / `max-*` with `var()` or `env()` was
printed before the plain fallback written above it, so the fallback won
(#42101). 26 real stylesheets
- Keep box-orient and box-direction when flex-direction or flex-flow
follows (#45672). 16 real stylesheets
- Write held prefixed background declarations at every flush (#46882). 6
real stylesheets
- Derive equality for Size2D and BorderRadius, and keep every prefix in
the border-radius handler (#45665). 2 real stylesheets
- Print `none` instead of an empty border-image, mask-border or
mask-box-image value (#45143). 2 real stylesheets
- Keep font/transition sub-properties on the side of the shorthand they
were written on (#41990). 2 real stylesheets
- CSS-wide keywords are not names: `font:12px Arial;font-family:inherit`
printed `font:12px inherit`, `color-scheme:inherit` and
`color-scheme:auto` printed `normal`, `transition-property:unset` was
folded into `transition` (#42022). 2 real stylesheets
- Only fold font longhands into `font` when the rule declared a `font`
(#45459)
- Keep a font family name quoted when its identifiers do not read back
as the same name (#46606)
- Stop rewriting valid positions into ones browsers reject (#45197)
- `border` is no longer built from longhands unless the rule also sets
what `border` resets (`border-image`), and an author's `border-image`
reset is no longer dropped. `transition` is likewise only written where
the rule wrote one, because it resets `transition-behavior` (#42072)
- `border; border-image: <gradient>; border-image-slice: 1` lost the
gradient in every default build (#42018)
- A prefixed `linear-gradient` keeps its direction, written or
generated: the prefixed syntax names the starting side and measures
angles differently (#46697)

**At-rules**

- Print @font-face format() keywords as strings (#37979). 439 real
stylesheets
- Print @supports `not` conditions without a leading space (#38559). 88
real stylesheets
- Reset after the number token of a unicode-range (#45835). 33 real
stylesheets
- Keep `<general-enclosed>` media and container conditions as written
(#46756). 24 real stylesheets
- A strict range such as `(width < 768px)` lowered for old targets still
matched at exactly 768px. The step is now `.02px`. The `not all and`
form for other units is not taken (#46806). 18 real stylesheets
- Keep scanning an @supports condition after a nested block (#38552). 9
real stylesheets
- Every `@counter-style name` printed as `@counter-stylename`, which
browsers drop (also #46692). In a CSS module the name now stays global,
like the `list-style` that refers to it (#33679). 9 real stylesheets
- Keep a keyframe whose block has an invalid item (#46288). 2 real
stylesheets
- Read an @Property initial-value on the stylesheet's parser, keep an
unmatched value as written (#45806). 2 real stylesheets
- `@media {}` is `all`. A media query that stops after `and`, `or` or
`not` (`@media screen and {`) is a build error; `main` printed `not all`
or dropped the condition, silently. The `@layer` and type selector parts
are not taken (#46869). 2 real stylesheets
- Accept a name-only `@container` and a `style()` feature with no value
(#46689). 1 real stylesheets
- Parse `<timeline-range-name> <percentage>` keyframe selectors (#45652)
- Allow `or` inside parentheses after a media type (#45088)
- Print the `=` comparison in media and container queries as `=`
(#45071)
- Parse the @font-palette-values font-family descriptor as a list of
family names (#45484)

**Selectors**

- Never wrap a selector list in :is() (#46787). 414 real stylesheets
- Stop emitting :-webkit-any() copies of :is() that outrank the original
rule (#42029). 373 real stylesheets
- With browser targets, a selector list with a vendor-prefixed member
was split, which revives hacks that rely on other engines dropping the
whole list: `.s *::-ms-backdrop,.s{flex:auto}` applied everywhere.
Rewritten; the list is printed as written (#42100). 42 real stylesheets
- Print the vendor prefix on downleveled :any-link, :read-only,
:read-write, :placeholder-shown and :autofill (#42131). 12 real
stylesheets
- Fix nesting lowering of a `&` that does not lead its selector
(#43047). 8 real stylesheets
- `@scope`: `&` and implicit selectors compile to `:where(:scope)`,
relative selectors and bare declarations parse (#43165). 7 real
stylesheets
- Only reject bare :global / :local pseudo-classes in CSS modules
(#38534). 5 real stylesheets
- Stop truncating `@scope to (...)` and stop replacing `&` in the
scope-end selector (#43146). 2 real stylesheets
- `[xl|href$=".pdf"]` lost its namespace prefix (#45159). 2 real
stylesheets
- Match An+B keywords exactly and take only `+` as the leading sign
(#42953). 1 real stylesheets
- Parse ::details-content and ::picker() like ::part() (#45504). 1 real
stylesheets
- Parse :window-inactive as an ordinary pseudo-class, and accept it
after ::selection (#45815)
- Print :nth-col(1) and :nth-last-col(1) in functional form (#41575)
- `li::before::marker` failed the build (#45810)
- `&` inside `:nth-child(... of S)` is resolved when nesting is compiled
away. The second selector serializer (326 lines) is deleted (#43024)

**Colors and `Bun.color`**

- Keep light-dark() as written under the default browser targets
(#42009). 227 real stylesheets
- Number and percentage channels parse in `lab()`, `lch()`, `oklab()`,
`oklch()`, `hsl()` and `hwb()`; the hue of `lch` / `oklch` is no longer
scaled by alpha (#45084). 130 real stylesheets
- `Bun.color` returns `null` for `currentColor`, system colors and
`light-dark()` in the converting formats, instead of the CSS keyword as
a string (#33047). 130 real stylesheets
- Relative colors: channel keywords are on the scale of their function
(`rgb(from #369 calc(r + 10) g b)`), the origin's alpha is kept, a
relative color kept as written gets no `@supports` copies, and a result
outside sRGB is not folded (#45362). 31 real stylesheets
- Keep rotate: 0deg out of none, and keep the origin alpha in relative
colors (#38486). 13 real stylesheets
- Compute the D50 reference white in f32 so greys convert to lab(l 0 0)
like lightningcss (#38535). 7 real stylesheets
- Return a missing hue for a grey from conversions into lch, oklch, hsl
and hwb (#45201). 5 real stylesheets
- Fix color(a98-rgb), srgb-linear relative colors and color-mix(in
xyz-d50) (#38487). 4 real stylesheets
- Match named colors without regard to ASCII case (#45092). 3 real
stylesheets
- Fold min(), max() and clamp() over plain numbers (#39515)
- `calc(l - .08)` in a percentage channel of a relative color became NaN
and printed `#000` (also #45970) (#45971)
- Bun.color: convert color() inputs instead of returning them as a CSS
string (#38488)
- A `color-mix(in srgb, ...)` result outside sRGB prints `color(srgb
...)` instead of a gamut-mapped hex (#39269)

**`calc()` and numbers**

- Rounding to 6 significant digits ate into the integer part:
`z-index:2147483647` became `2147480000`, `max-width:9999999px` became
`10000000px`, `--x:1234567.5` became `1234570`. It now only cuts a
fraction short: a value kept as tokens prints its source text, a typed
value prints every integer digit and at least one digit of the fraction
(#46715). 43 real stylesheets
- A number without a unit is no longer repaired into `px`
(`height:1.5715` is dropped by a standards-mode browser, so it prints as
written), `calc(-1 * 0)` is not a length, and on a property of the range
[0,∞] (sizes, border radii, padding, border and outline widths, gap,
`flex-basis`, `font-size`) a `calc()` that folds below zero prints `0`
instead of an invalid negative literal, and a negative literal no longer
replaces the valid declaration before it (`width:10px;width:-5px`
printed `width:-5px`) (#42104). 24 real stylesheets
- Print numbers from the f32's shortest digits, not its f64 expansion
(#38565). 17 real stylesheets
- Print negative numbers below 1 with a single minus sign (#38516)
- Keep math functions over two different length units as written
(#38489)
- Fold rem() with a truncated quotient, so it keeps the sign of the
dividend (#32286)
- A one-argument `min()` / `max()` around a sum printed without any
function (`width:100% - 20px`) (#46145)
- `round()` breaks ties upward and handles a negative step as browsers
do (#45249)

**CSS modules**

- A CSS module exports its `@keyframes` names (`styles.spin`). Hashing
is unchanged (#46720). 145 real stylesheets
- A name inside `:global(...)` prints plain but was exported as
`name_<hash>`, a class no rule has (also #38573) (#45487). 90 real
stylesheets
- Keep container names global in CSS modules (#45247). 6 real
stylesheets
- Tag the keyframes name when an `animation` value falls back to raw
tokens (#46034). 6 real stylesheets
- In a plain stylesheet `composes: a from "./b.css"` was treated as an
import: the file was inlined and an absolute path printed (#45961). 4
real stylesheets
- Drop a rejected composes instead of failing the file at print time
(#38538). 1 real stylesheets
- Keep every rule's custom properties for the composes conflict check
(#38523)
- Keep composes working after a nested rule in the same block (#38520)
- `IdentOrRef::hash` read 2 of 16 bytes, so in release builds every
class of a module hashed alike (#30784)

**Tokens, `url()`, diagnostics**

- The block skip stack no longer allocates on every block exit (#39533)
- Report the diagnostic's line without its line breaks in
position.lineText (#38342). 192 real stylesheets
- Compare `url()` values by their text (#46895). 26 real stylesheets
- Values kept as tokens keep the blanks they were written with instead
of having them re-invented at print time: `attr(x type(<number>))`
printed `type(< number> )`, which browsers drop. Minified output still
drops the blanks that cannot matter (#45577). 19 real stylesheets
- The IE star hack (`*zoom:1`) failed the whole build. The declaration
is skipped, as every browser does. It is the one syntax error that
builds: tachyons, handsontable and jasmine ship it (#46751). 8 real
stylesheets
- The path of a copied asset spliced into `url()` is always quoted, so a
file name with a space or a parenthesis cannot end the `url()` (#41557).
7 real stylesheets
- Move the skip loop of parse_until_before out of the generic body
(#39520)
- Remove dead code from bun_css: the unused `impl Default for Property`
(#45618)
- Default the at-rule and qualified-rule parser traits, drop the
url_to_css shim (#31999)

**Speed**

Four inputs that took quadratic or exponential time, or produced output
out of proportion, are now linear, with no new limit and no extra pass
(#31639, #42243, #38531, #43421).

**Tests only**

- `css-fuzz.test.ts` never ran in CI and timed out on debug builds; it
is a deterministic table now (#38500)
- `nth-anplusb-ident.test.ts` counts heaps in a child process (#46331)
- `color.test.ts` checks the ansi-16 sweep with one `expect()` (#42824)
- Not CSS, but it failed this PR's CI and fails on `main` too:
`ws-proxy.test.ts` on Windows, "a message sent during a TLS
renegotiation arrives after it". The helper server now renegotiates from
the callback of the frame's write (#46170)

**No pull request covered these**

- `--v: 5.0.1` printed as `5.1`, `--w: 1.0.0` as `10`, `--x: 1+2` as
`12`, `2n+1` as `2n1`: a number token that touches another keeps its
source text.
- `color-scheme: auto` printed as `normal`, and `color-scheme: dark
light` as `light dark`.
- A bare `:global` or `:local` in a CSS module sets the scope of the
names after it, up to the end of the selector, as in css-loader. It was
a build error.
- The scrollbar states (`a:horizontal`,
`::-webkit-scrollbar{&:horizontal{}}`) are valid in a plain compound, as
in WebKit and Blink.
- Without browser targets, `-webkit-box-align`, `-ms-flex-align` and the
like were dropped next to their standard form (flatpickr,
foundation-sites, materialize-css: 28 stylesheets), and
`-webkit-fill-available` before `-moz-available` (beercss).
- `(-o-min-device-pixel-ratio: 2/1)` printed as `2`.
- `sin()`, `cos()` and `tan()` are exact at multiples of 90deg:
`calc(100px * cos(90deg))` printed `-.00000437114px`.
- A `display-p3` color at targets that have P3 but not the other
`color()` spaces (Safari 10.1 to 14) got itself as a fallback.
- `width: rem(-18px, 5px)` folds to a negative literal once `rem()` is
right; it prints `0`.
- Consecutive nested declarations rules, which arise when same-query
`@media` blocks in one rule merge, are folded into one and count against
the existing selector expansion limit as the `&{}` rules they replace
did.

#### Known and not fixed here

- Four longhands are still folded into a `transition` the author did not
write where the targets add a prefixed copy of a property (`mask-image`
at Chrome 80): only the shorthand gives the copy its own times.
- A later `background`, `box-shadow` or `border-*` declaration still
loses to the `@supports` copy of an earlier `var()` value of the same
property with a wide-gamut color. Same on `main`; #45560 is taken for
the properties of the fallback handler and for unparsed values only.
- A merged rule prints the unprefixed name in the passes of the rule it
absorbed: `.d::-webkit-input-placeholder{}.d::-moz-placeholder{}` prints
`::placeholder` for the second (#42106, not taken: it breaks the merge
of prefix-equivalent rules).
- The `border`, `outline` and `font` shorthands, `line-height`,
`scroll-padding`, `background-size` and a few more still print a
`calc()` that folds below zero as a negative literal. No real stylesheet
has one.
- `all` is not a property Bun knows, so `a{margin-top:1px;all:unset}`
prints `all` first. Same on `main`.
- A selector kept as raw tokens loses the blank before `*` when
minified: `--sel: .dark *` prints `.dark*`. Same on `main`.
- `width:calc(0% + 12px)` folds to `12px`, which lays out differently in
a table cell. Same on `main`.
- The `:dir()` / `:lang()` rules of a lowered logical property print
before the nested rules of their rule, so a logical declaration written
after a nested rule of equal specificity still loses to it. Same on
`main`.
- A fraction is still rounded to 6 significant digits (`12345.678`
prints `12345.7`, `33.3333333333%` prints `33.3333%`). Keeping every
digit would add up to 282 KB over the corpus.
- `@keyframes "quoted"` is hashed in a module but not exported.
- In a module, a keyframes name inside a `var()` fallback is not hashed:
`animation: var(--enter, fade 1s)`. Same on `main`.
- A `color-mix()` or relative color folded to a hex inside a gradient
changes the interpolation from Oklab to sRGB (VS Code, clawhub).
Printing `color(srgb ..)` instead needs a fallback for `mask-image`,
`list-style-image` and custom properties at the default targets, which
have none.
- A comma list in `@container` still fails the build.
- After a bare `:global`, names that are hashed at print time
(`::view-transition-old(foo)`) are still hashed; inside `:global(...)`
they are not.

#### Pull requests in scope that do not land here

- Duplicates of a pull request that lands here: #36961 (of #37979),
#38478 (of #46662), #38480 (of #46733), #38482 (of #46715), #38491 (of
#38500), #38508 (of #45201), #38513 (of #45362), #38573 (of #45487),
#38640 (of #38500), #45525 (of #45247), #45589 (of #46756), #45970 (of
#45971)
- Already fixed on `main`: #38674
- Right, but for input nobody writes (0 of 10,116 stylesheets, and not
new syntax): #31916, #38526, #38592, #39349, #41139, #41801, #42042,
#42484, #43078, #45604, #45908, #46086, #46221, #46224, #46273, #46694,
#46707, #46737, #46755, #46816, #46870, #46934
- Right or harmless, but not worth their code, their bigger output or
the behavior they change: #30905, #31924, #33332, #34426, #36225,
#38006, #38512, #38547, #38551, #38561, #38572, #38576, #38607, #38623,
#38667, #38692, #42106, #42912, #42918, #43407, #43434, #44700, #45442,
#45546, #45747, #45759, #45825, #45946, #46061, #46127, #46206
- Need a decision (see above), stay open: #40368, #40369, #44647,
#44648, #45814
- Stays open: #45787

Out of scope: CSS linking and chunking in the bundler, the dev server,
docs-only pull requests, and repository-wide sweeps that touch `src/css`
in passing.

Thanks to @mikamikasuki (#45247), @alii (#39520, #31999) and @oddharsh,
whose measurements on #40369 and #41990 were used.

Fixes #16727.
Fixes #24770.
Fixes #30772.
Fixes #44704.
Fixes #45089.
Fixes #45237.
Fixes #46872.

### How did you verify your code works?

- The 35 CSS test files pass on a debug build, also with
`FORCE_COLOR=1`. 600+ of the tests in this branch fail on a build of
`main`. An earlier head of this branch passed all 179 CI jobs that ran.
- The reproduction of each issue listed above was run on `main` (wrong)
and on this branch (right).
- Differential run over the 10,116 stylesheets, `main` against this
branch, minified without targets and through `Bun.build` at the default
targets: no stylesheet that builds on `main` fails; Chromium 156
compares the output with the input rule by rule (CSSOM, then computed
values on a sized element in a standards-mode document, colors as
painted pixels in sRGB and P3). The comparison has 195 control pairs
that must read equal or different. For stylesheets that read differently
on both builds, mostly because of lowering, the sets of differences were
compared; the three new ones are two minifier fixtures that are wrong on
both and one comparison artifact.
- CSS modules are checked for consistency instead, since hashed names
defeat the comparison: every reference points at a name the output
defines, every export exists.
- `Bun.color`: 59 inputs in 13 formats on both builds; every changed
cell was read.
…prefix-handler

# Conflicts:
#	src/css/properties/properties_generated.rs
#	src/css/rules/style.rs
#47025 prints the vendor prefix on a downleveled `:autofill`, so the
vendor prefix section no longer lists it as missing.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants