Skip to content

Functional pseudo selectors drop required parentheses #447

@raymondanythings

Description

@raymondanythings

Summary

  • functional pseudo selectors generated by the extractor drop the required parentheses when another selector is nested (e.g. _not={{ _lastOfType: {} }} becomes :not:last-of-type)
  • CSS parsers reject the emitted selector, so builds that rely on Lightning CSS / PostCSS currently fail
  • the issue impacts every pseudo selector that requires parentheses such as _not, _is, _where, _has, _nthChild, _lang, etc., preventing us from styling with those helpers

Steps to Reproduce

  1. Render any component with the pseudo helper, for example:
    <Box
      _not={{
        _lastOfType: {
          color: 'tomato',
        },
      }}
    />
  2. Run the extractor via the build/dev toolchain (e.g. pnpm dev in apps/landing).
  3. Inspect the generated CSS or let Lightning CSS validate it – the output contains a selector fragment &:not:last-of-type.

Expected Behavior

  • Nested pseudo selectors should produce &:not(:last-of-type) so that the child selector is wrapped in parentheses.

Actual Behavior

  • The extractor yields &:not:last-of-type (missing parentheses) which is rejected by Lightning CSS with Expected "(" after ":not" and by browsers because it is not valid CSS.

Root Cause Analysis

  • When _not (or any pseudo helper) is processed, we recurse via extract_style_from_expression and chain the current selector with the new one (libs/extractor/src/extractor/extract_style_from_expression.rs:249).
  • Chaining delegates to StyleSelector::from([base, next]), which simply concatenates the pieces using SelectorSeparator (libs/css/src/style_selector.rs:158).
  • The formatter emits &:not:last-of-type because SelectorSeparator can only return : / :: / empty (libs/css/src/selector_separator.rs:22), so there is no opportunity to insert parentheses before appending the new selector.
  • No special casing exists for functional pseudo classes (e.g. :not, :is, :where, :has, :nth-child, :lang), so every nested use results in invalid selectors.

Impact

  • Any component relying on functional pseudo selectors fails to compile, so patterns like _not, _is, _where, _has, _nthChild, _nthLastOfType, _nthCol, _lang, _dir, etc. cannot be used.
  • Docs currently encourage these helpers, but attempting to use them blocks builds in Next.js / Vite because Lightning CSS throws a syntax error.
  • There is no workaround within the design system other than inlining raw CSS selectors by hand, defeating the zero-install goal.

Suggested Fix

  1. Track functional pseudo selectors and wrap the chained selector string in parentheses whenever the base selector ends with one of those functions (e.g. format!("{base}({body})")).
  2. Alternatively, represent selectors as structured AST nodes instead of raw strings so that formatting can distinguish between pseudo classes and functions before serialisation.
  3. Add snapshot/unit coverage for _not + _lastOfType, _is with multiple selectors, _nthChild with numeric arguments, etc., to prevent regressions.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingrustPull requests that update rust code

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions