Repository navigation
Render css() definitions as nested css - #896
Conversation
schultek
left a comment
There was a problem hiding this comment.
Thanks for taking this.
Could you rebase your changes onto next branch and then target next for this PR. That branch is where the next major version is being developed on.
02c4131 to
3700e66
Compare
|
Done, rebased onto One heads up: I couldn't run the suite on |
3700e66 to
4288955
Compare
Package Version ReportThe following packages have been updated: |
There was a problem hiding this comment.
🟡 Changes recommended
Conditional styling of pseudo-elements regresses when bare-& rules appear inside grouping at-rules.
2 open findings
What changed in this PR
Updates css() to preserve native CSS nesting, addressing #578 while keeping resolve() flattening unchanged.
Changes:
- Renders nested rules directly, folds direct bare-
&children, and omits empty rules. - Adds rendering and browser computed-style tests.
- Documents the output change and selector-list specificity implications.
| File | Description |
|---|---|
packages/jaspr/test/dom/styles/rules_test.dart |
Updates output expectations and tests nesting behavior. |
packages/jaspr/test/dom/styles/nested_css_browser_test.dart |
Adds browser checks for nested styles. |
packages/jaspr/lib/src/dom/styles/css.dart |
Implements nested rendering and documents its output. |
packages/jaspr/CHANGELOG.md |
Adds release notes and compatibility guidance. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
schultek
left a comment
There was a problem hiding this comment.
The copilot review seems valid.
Also could you remove the old flattening logic in css.dart.
A definition already looks nested in Dart, and until now it was flattened on
the way out: every rule under a parent became its own selector. Css nesting
has been in all major browsers since 2023, so the nesting can survive into
the stylesheet, at-rules included.
Two details fall out of it. A child whose selector is just `&` is the parent,
so what it holds is written into the parent's block rather than a block of its
own — which is how `css('.x', [css('&').styles(...)])` keeps rendering the
same css as before. And a rule with neither properties nor children renders
nothing at all rather than an empty block.
The output is equivalent for a single parent selector. A parent that is a
selector list is not: a nested selector resolves through `:is()`, which takes
the specificity of its most specific argument.
`resolve` still flattens. It is not exported from `package:jaspr/dom.dart`,
and `jaspr_content` uses it to wrap the selectors of its content theme.
Fixes schultek#578
A bare `&` inside a `@media`, `@supports` or `@layer` nested in a rule now
writes its declarations straight into that block. Wrapped in `& {}` they did
not reach a pseudo-element parent, since `&` matches like `:is()`.
The flattening `resolve()` is gone. `NestedStyleRule` exposes its selector,
own styles and children, and the content typography scopes each parent
selector with `_scopeRule` instead of resolving the rules first. The field is
`ownStyles` because `styles` is the `StylesMixin` builder `css()` chains.
4288955 to
08fa946
Compare
|
Done. A bare |
schultek
left a comment
There was a problem hiding this comment.
Did a small refactor, otherwise LGTM


Description
css()now renders nested definitions as nested css, as asked in #578.A definition already looks nested in Dart and was flattened on the way out — one selector per rule. Now the nesting survives into the stylesheet:
Two details fell out of it:
&is the parent, so what it holds is written into the parent's block instead of opening one of its own. That keepscss('.x', [css('&').styles(...)])— the shape the docs use andjaspr_contentbuilds on — rendering the same css as before.resolveis untouched and still flattens. It is not exported frompackage:jaspr/dom.dart, andjaspr_contentuses it to wrap every selector of its content theme in:where(...):not(...), which still works — its tests pass unchanged.What changes for users
The rendered css is equivalent for a single parent selector. It is not equivalent when the parent is a selector list: a nested selector resolves through
:is(), which takes the specificity of its most specific argument, where the flattened form kept each selector's own. Worth a line in the release notes.Anything that asserts on rendered css output will see the new shape. In this repo that was one test.
Type of Change
Tests
test/dom/styles/rules_test.dart, seven new cases: a nested selector stays nested, a&child folds into the parent, three levels deep, a nested at-rule stays inside the rule, a parent with only children still opens its block, a rule with nothing in it renders nothing, andresolvestill flattens.test/dom/styles/nested_css_browser_test.dart, four new cases that put the rendered css in a<style>element and readgetComputedStyleback, so the claim is that Chrome applies it rather than that the string looks right: a nested class selector, a nested pseudo-class, a nested@mediathat wins over the outer declaration, and three levels deep.dart test -p vm(188) anddart test -p chrome(35) are green inpackages/jaspr, anddart test(75) inpackages/jaspr_content.Fixes #578