Skip to content

Render css() definitions as nested css - #896

Merged
schultek merged 3 commits into
schultek:nextfrom
diegolopezrm:feat/nested-css
Oct 20, 2026
Merged

schultek merged 3 commits into
schultek:nextfrom
diegolopezrm:feat/nested-css

Conversation

@diegolopezrm

Copy link
Copy Markdown
Contributor

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:

css('.someclass', [
  css('&').styles(width: 100.px),
  css('&:hover').styles(backgroundColor: Colors.blue),
  css('p').styles(fontSize: 2.em),
  css.media(MediaQuery.screen(minWidth: 1000.px), [css('&').styles(width: 200.px)]),
])
.someclass {
  width: 100px;
  &:hover {
    background-color: blue;
  }
  p {
    font-size: 2em;
  }
  @media screen and (min-width: 1000px) {
    width: 200px;
  }
}

Two details fell out of it:

  • A child whose selector is just & is the parent, so what it holds is written into the parent's block instead of opening one of its own. That keeps css('.x', [css('&').styles(...)]) — the shape the docs use and jaspr_content builds on — rendering the same css as before.
  • A rule with neither properties nor children renders nothing rather than an empty block, since with nesting a parent has to open a block for its children even when it says nothing itself.

resolve is untouched and still flattens. It is not exported from package:jaspr/dom.dart, and jaspr_content uses 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

  • ✨ New feature or improvement

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, and resolve still flattens.

test/dom/styles/nested_css_browser_test.dart, four new cases that put the rendered css in a <style> element and read getComputedStyle back, 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 @media that wins over the outer declaration, and three levels deep.

dart test -p vm (188) and dart test -p chrome (35) are green in packages/jaspr, and dart test (75) in packages/jaspr_content.

Fixes #578

@schultek schultek left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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.

@diegolopezrm
diegolopezrm changed the base branch from main to next September 24, 2026 11:39
@diegolopezrm

diegolopezrm commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Done, rebased onto next and retargeted. Only the changelog conflicted; my entry sits under Behavior changes now, shortened to match the ones around it.

One heads up: I couldn't run the suite on next locally, since it wants Dart 3.13 and I'm on 3.12.2, so CI is the first real run. The styles code is the same on both branches (the diff there is only properties/filter.dart, properties/spacing.dart and five lines of styles.dart), and everything passed on main, vm, chrome and jaspr_content alike, so I don't expect surprises. I'd rather say it than imply I checked.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Package Version Report

The following packages have been updated:
jaspr_test : 0.23.5 -> 0.24.0
jaspr : 0.23.5 -> 0.24.0
jaspr_builder : 0.23.5 -> 0.24.0
jaspr_cli : 0.23.5 -> 0.24.0
jaspr_router : 0.9.0 -> 0.10.0
jaspr_content : 0.5.4 -> 0.6.0
jaspr_flutter_embed : 0.4.11 -> 0.5.0
jaspr_lints : 0.7.2 -> 0.8.0
jaspr_riverpod : 0.4.6 -> 0.5.0
jaspr_serverpod : 0.6.1 -> 0.7.0

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Comment thread packages/jaspr/CHANGELOG.md Resolved

@schultek schultek left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor Author

Done. A bare & in a nested @media, @supports or @layer now writes straight into that block, with a browser test on a ::before parent that fails without it. resolve() and the flattening are gone, and typography.dart uses your _scopeRule. One change to the snippet: the field is ownStyles, since styles is the StylesMixin builder that css('.a').styles(...) calls. The changelog now covers the &-active case, and the branch is rebased onto next.

@schultek schultek left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Did a small refactor, otherwise LGTM

@schultek
schultek merged commit ce97cdb into schultek:next Oct 20, 2026
3 checks passed
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.

feat: css() should produce nested css definitions

3 participants