Skip to content

css: recognize ::details-content, ::picker(), ::checkmark and ::picker-icon - #41122

Merged
Jarred-Sumner merged 1 commit into
mainfrom
robobun/b22fd51a/css-pseudo-element-table
Sep 1, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
robobun/b22fd51a/css-pseudo-element-table

Conversation

@robobun

@robobun robobun commented Sep 1, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun build warns Invalid selector. Unsupported pseudo-class or pseudo-element 'details-content' (and the same for picker, checkmark, picker-icon) on valid CSS. The emitted CSS is correct, only the diagnostic is wrong.
  • The cause: the pseudo-element lookup tables in src/css/selectors/parser.rs trail lightningcss 1.33.0, the source this parser is ported from. lookup_pseudo_element (line 1528) misses details-content, picker-icon, checkmark. The functional table in parse_functional_pseudo_element (line 1238) misses picker().

Fix

  • Add the four entries. Each is a real PseudoElement variant with its own serialization (DetailsContent, PickerIcon, Checkmark, PickerFunction { identifier: Ident }), the same shape lightningcss uses, not a mapping to Custom.
  • ::picker() parses one <ident> argument, as upstream does. The simple entries go through the existing case-insensitive table, so ::DETAILS-CONTENT now serializes as ::details-content.
  • The scroll-marker family (::scroll-marker, ::scroll-marker-group, ::scroll-button()) from the issue still warns. lightningcss warns on those too, including current master, so this PR stays at upstream parity.
  • Verified: test/bundler/css/forms-pseudo-elements-41120.test.ts (both tests fail on 1.4.1). Also ran test/bundler/css/ (174 pass) and test/js/bun/css/ (failures there are pre-existing fuzz-test timeouts, identical on an unpatched build).

Background

  • Bun's CSS selector parser is a port of lightningcss. An unknown pseudo-element parses as PseudoElement::Custom, passes through unchanged, and warns unless it is vendor-prefixed. The warning path is intentional and is not touched here.
  • ::details-content is css-pseudo-4. ::picker(), ::picker-icon, ::checkmark are css-forms-1 (the customizable <select> styling). All four ship in Chromium stable.
  • The functional table is a length-gated case-sensitive match (see the PERF comment at the parse site). The picker entry follows that shape, with the comment updated for the new length set.
Notes
  • Upstream reference: parcel-bundler/lightningcss v1.33.0 src/selector.rs: details-content (line 284), picker-icon (line 310), checkmark (line 311) in the simple table, picker in the functional table (line 339). No scroll-marker family entries there, on the tag or on master.
  • :target-current from the issue is quiet in Bun today because bare pseudo-class warnings gate on a _ prefix instead of -. css: warn about unknown bare pseudo-classes unless they are vendor prefixed #38572 fixes that gate separately. Upstream gates :target-current behind a non-default scroll_navigation_controls option, so no entry is added here.
  • Behavior edge: ::picker(not an ident) now fails selector parsing instead of parsing as a custom function. lightningcss behaves the same way.
  • Only src/css/selectors/parser.rs and src/css/selectors/selector.rs match on these variants. CssEql/CssHash come from derives. The targets-compat check in selector.rs treats the new variants like other post-selectors-4 pseudo-elements (not downlevelable), unchanged from upstream.

[human-review] gate passed · iteration 0 · 3 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" "test/bundler/css/forms-pseudo-elements-41120.test.ts"
bun test v1.4.1 (a6c4cc276)

test/bundler/css/forms-pseudo-elements-41120.test.ts:
20 |     const result = await Bun.build({
21 |       entrypoints: [path.join(String(dir), "in.css")],
22 |       minify: true,
23 |       throw: true,
24 |     });
25 |     expect(result.logs.map(String)).toEqual([]);
                                         ^
error: expect(received).toEqual(expected)

- []
+ [
+   "BuildMessage: Invalid selector. Unsupported pseudo-class or pseudo-element 'details-content'",
+   "BuildMessage: Invalid selector. Unsupported pseudo-class or pseudo-element 'picker'",
+   "BuildMessage: Invalid selector. Unsupported pseudo-class or pseudo-element 'checkmark'",
+   "BuildMessage: Invalid selector. Unsupported pseudo-class or pseudo-element 'picker-icon'",
+ ]

- Expected  - 1
+ Received  + 6

      at <anonymous> (/workspace/bun/test/bundler/css/forms-pseudo-elements-41120.test.ts:25:37)
(fail) css > known form pseudo-elements do not warn (#41120) [167.32ms]
39 |     cons
... (truncated)

release without fix: 2 FAILED
bun test v1.4.1-canary.1 (a6c4cc276)

test/bundler/css/forms-pseudo-elements-41120.test.ts:
20 |     const result = await Bun.build({
21 |       entrypoints: [path.join(String(dir), "in.css")],
22 |       minify: true,
23 |       throw: true,
24 |     });
25 |     expect(result.logs.map(String)).toEqual([]);
                                         ^
error: expect(received).toEqual(expected)

- []
+ [
+   "BuildMessage: Invalid selector. Unsupported pseudo-class or pseudo-element 'details-content'",
+   "BuildMessage: Invalid selector. Unsupported pseudo-class or pseudo-element 'picker'",
+   "BuildMessage: Invalid selector. Unsupported pseudo-class or pseudo-element 'checkmark'",
+   "BuildMessage: Invalid selector. Unsupported pseudo-class or pseudo-element 'picker-icon'",
+ ]

- Expected  - 1
+ Received  + 6

      at <anonymous> (/workspace/bun/test/bundler/css/forms-pseudo-elements-41120.test.ts:25:37)
(fail) css > known form pseudo-elements do not warn (#41120) [3.86ms]
39 |     const result = await Bun.build({
40 |       entrypoints: [path.join(String(dir), "in.css")],
41 |       minify: true,
42 |       throw: true,
43 |     });
44 |     expect(result.logs.m
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" "test/bundler/css/forms-pseudo-elements-41120.test.ts"
bun test v1.4.1 (a6c4cc276)

test/bundler/css/forms-pseudo-elements-41120.test.ts:
(pass) css > known form pseudo-elements do not warn (#41120) [146.12ms]
(pass) css > the pseudo-element lookup is case-insensitive (#41120) [86.75ms]

 2 pass
 0 fail
 4 expect() calls
Ran 2 tests across 1 file. [2.40s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     4a4430180f
  features     baseline

23 deps, 131 codegen, 1172 objects in 736ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1244] install /workspace/bun
bun install v1.4.1-canary.1 (a6c4cc276)

Checked 26 installs across 63 packages (no changes) [14.00ms]
[2/1244] gen bindgenv2
[3/1244] fetch zlib
[zlib] up to date
[4/1244] fetch tinycc
[tinycc] up to date
[5/1243] fetch libjpeg-turbo
[libjpeg-turbo] up to date
[6/1216] install /workspace/bun/packages/bun-error
bun install v1.4.1-canary.1 (a6c4cc276)

Checked 1 install across 2 packages (no changes) [1.00ms]
[7/1216] gen bake.{client,server,error}.js
-> bake.client.js, bake.server.js, bake.error.js
[8/1216] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[9/1216] gen ErrorCode+*.h
[10/1216] install /workspace/bun/src/node-fallbacks
bun install v1.4.1-canary.1 (a6c4cc276)

Checked 111 installs across 104 packages (no changes) [9.00ms]
[11
... (truncated)
diff hotspot
src/css/selectors/parser.rs                        | 25 ++++++++++-
 src/css/selectors/selector.rs                      |  8 ++++
 .../css/forms-pseudo-elements-41120.test.ts        | 48 ++++++++++++++++++++++
 3 files changed, 80 insertions(+), 1 deletion(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                                  reads  edits  tests
src/css/selectors/parser.rs                               2      2      4
src/css/selectors/selector.rs                             2      1      4
test/bundler/css/forms-pseudo-elements-41120.test.ts      0      2      4

root cause · written by the author bot

The pseudo-element lookup tables in the CSS selector parser were ported from an older snapshot of lightningcss and lacked entries that upstream later added, so ::details-content, ::picker(), ::checkmark and ::picker-icon fell through to the fallback path that emits an "unsupported pseudo-element" warning while still passing the selector through as a custom pseudo-element. The fix adds these four names as proper pseudo-element variants with upstream-matching serialization, including registering picker() in the functional pseudo-element table, so they parse cleanly without warnings.…

…r-icon

These pseudo-elements are defined in css-pseudo-4 and css-forms-1 and
are in lightningcss 1.33.0's lookup tables. Bun's port missed them, so
it warned 'Unsupported pseudo-class or pseudo-element' on valid CSS.

Add the three simple entries to lookup_pseudo_element and the picker()
entry to the functional table. Each is a real PseudoElement variant
with its own serialization, as upstream does.

Fixes #41120

@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.

Comment thread test/bundler/css/forms-pseudo-elements-41120.test.ts
@oddharsh

oddharsh commented Sep 1, 2026

Copy link
Copy Markdown

Scope looks right to me, thanks for the fast turnaround. Two follow-ups, and the parity rule puts them in different places.

:target-current is the row that regresses. Bun has no TargetCurrent and no scroll_navigation_controls flag at all, while lightningcss master carries all three of :target-current / :target-before / :target-after behind that flag (src/selector.rs:157, flag read at :103). Bun is quiet on :target-current today only because bare pseudo-classes warn on a _ prefix, which #38572 corrects to the upstream - rule. So #38572 landing turns it into a new false positive, and this one is a straight parity gap rather than a divergence. I am putting up a PR that ports the flag and the three entries.

The scroll-marker family I will take upstream instead. You are right that lightningcss master lacks ::scroll-marker, ::scroll-marker-group and ::scroll-button(), so asking Bun to diverge is the wrong door. It is tracked there as parcel-bundler/lightningcss#1177 and #1184, and I will send it against those and link back here, so it reaches Bun through the port rather than around it.

@robobun

robobun commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Sounds good. This PR stays at the four parity entries.

One note for the :target-current port: lightningcss accepts it only when the scroll_navigation_controls option is set, and that option is off by default. Bun does not surface a switch for it in bun build today. So the port needs a decision: hard-enable the three entries, or carry the flag and pick a default. Worth stating in your PR body.

Linking the lightningcss PRs here works well. Once they land upstream, the scroll-marker family reaches Bun through the normal table updates.

@Jarred-Sumner
Jarred-Sumner merged commit b7039c9 into main Sep 1, 2026
11 of 13 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/b22fd51a/css-pseudo-element-table branch September 1, 2026 22:41
@oddharsh

oddharsh commented Sep 2, 2026

Copy link
Copy Markdown

Both linked, and the flag question has an answer I can measure rather than pick.

Hard-enable. bun build cannot set a CSS parser flag today. ParserFlags in src/css/css_parser.rs:2989 carries three bits (NESTING, CUSTOM_MEDIA, DEEP_SELECTOR_COMBINATOR), the one ParserOptions construction in the crate uses ParserFlags::default(), and repo-wide the type appears only in src/css/css_parser.rs, src/css/lib.rs, src/css/selectors/parser.rs, src/css_jsc/css_internals.rs and two test files. Nothing in the bundler path sets a bit. So a fourth bit would be unreachable from bun build, :target-current would keep warning once #38572 lands, and the port would buy nothing. Carrying the flag defaulted ON is hard-enabling plus a bit nobody can flip, so it is the same thing with more surface.

The cost is not only the diagnostic, and I would rather state it up front. Recognizing the three moves them out of Custom and onto the compat path, and src/css/compat.rs has no TargetCurrent or TargetBeforeAfter feature, so the port has to generate those alongside. Once it does, a stylesheet that sets old targets gets the :is() lowering lightningcss applies to :target-before, :target-after, where today Bun passes them through untouched. That is a change to emitted CSS rather than to a warning, so it will land with a targets test pinning both directions.

Ordering: this wants to go in after #38572, since that is what makes the gap visible.

The scroll-marker family is now up at parcel-bundler/lightningcss#1321, against #1177. It adds the three pseudo-elements behind the SCROLL_NAVIGATION_CONTROLS flag that already gates the :target-* pseudo-classes, plus one fix the feature turned out to need: ::scroll-marker:target-current was rejected, and that shape is what css-overflow-5 defines the pseudo-classes for. If it lands, the family reaches Bun through a table update the way you described.

@oddharsh

oddharsh commented Sep 2, 2026

Copy link
Copy Markdown

Opened as #41139, hard-enabled, with the ParserFlags reasoning in the body.

One correction to what I wrote above, since it was an objection I raised and it does not hold. I said recognizing these would turn on :is() lowering under old targets and so change emitted CSS. It does not. In is_compatible, Component::NonTsPseudoClass sends PseudoClass::Custom { .. } => {} and _ => {} to the same trailing return false, so an unmapped known variant and an unknown custom one already behave identically. Adding Feature::TargetCurrent / TargetBeforeAfter would be the behaviour change rather than omitting them, and compat.rs is generated, so #41139 adds none and matches this PR's scope.

The scroll-marker family is at parcel-bundler/lightningcss#1321 against their #1177.

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.

3 participants