CSS and Layout skill 7 of 12
CSS Nesting SKILL.md: Convert and Fix Native Nested CSS
The CSS Nesting skill makes Claude, Cursor or another AI agent convert flat CSS to native nested CSS and find out why a nested rule does nothing. A linter flags Sass-style &__suffix, declarations after nested rules and id-weighted lists, then an equivalence check compares computed styles before and after the rewrite.
css-nesting/SKILL.md, version 1.0.0, 2,529 words, plus scripts/nesting_lint.py, scripts/equiv_check.py and one reference file.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 30 October 2026. Updated 30 October 2026. Free to use under CC BY 4.0.
Work in this order: lint the stylesheet, decide what is worth nesting, write declarations first and nested rules after, settle the browser support line, then prove a refactor changed no computed style by comparing the old and new sheets in Chromium. Do not trust a nested rewrite until equiv_check.py reports 0 differences.
Measured values and source notes are in references/appendix.md.
When to use this skill, and when not to
Use it to convert flat CSS to nested CSS, to write new nested components, or to find out why a nested rule has no effect.
| Request | Use instead |
|---|---|
Specificity of :is(), :has() and :where() selectors | modern-css-selectors.md |
@layer order and which layer wins | cascade-layers.md |
| Container query syntax and sizing | container-queries.md |
| SCSS, Less or PostCSS configuration and builds | Out of scope. Ask for the compiled CSS |
| Naming convention debates (BEM, utility classes) | Out of scope |
Guardrails
- Propose, then change. Report and plan first. Edit only after the user approves a batch, on a branch. Never push, merge or deploy.
- Bulk edits need confirmation. Converting more than one stylesheet, or more than 10 rules, is shown as a diff first. Keep the original file until
equiv_check.pypasses. - Data handled. The tools read CSS and HTML files you pass.
nesting_lint.pyprints selectors and line numbers.equiv_check.pyprints element paths and property names, never text, field values or storage. Open only pages the user owns, with test data. Never request session cookies or tokens. - Nothing is invented. Every rule carries a source or a measurement. Version-dependent behaviour carries verify before relying on this.
- Not a conformance statement. A clean run means the checked page renders the same in Chromium. Safari and Firefox need their own check.
- Refuses to: nest SCSS syntax as if it were CSS, convert a file that has no equivalence check available without telling the user, claim browser support from memory, or fix a nesting problem by adding
!important.
Inputs the agent must have before starting
Ask for every missing item. Do not assume.
| Input | Ask exactly |
|---|---|
| Files | "List the stylesheets, in load order. Are any of them compiled from Sass or Less?" |
| Goal | "Convert existing CSS, write new nested CSS, or fix a rule that does not apply?" |
| Browsers | "Which browsers and minimum versions must work? Is there any engine older than Chrome 120, Safari 17.2 or Firefox 117?" |
| Markup | "Give me a page of markup I may open locally, without the stylesheets under test linked in it." |
| Depth | "Is a nesting depth limit of 3 acceptable, or is there a house limit?" |
| Tooling | "Is Python 3 installed? Is Playwright with Chromium installed for the equivalence check?" |
| Approval | "May I edit on a branch in batches and show each diff first?" |
Raw SKILL.md file
This is the exact file agents read, including the credit header and licence notice. Save it as css-nesting/SKILL.md.
---
name: css-nesting
description: "Writes and refactors native CSS nesting: the & selector, nested @media and @container, declarations after nested rules, bare type selectors, Sass-style &__suffix that silently fails, nested selector lists that gain id weight, and depth. Runs a read-only linter and a computed-style equivalence check in Chromium. Use for 'convert to nested CSS', nesting not applying, SCSS to native CSS. Do NOT use for Sass compilation, @layer order, :has() or :is() design, or PostCSS setup."
license: CC-BY-4.0
metadata:
author: Afeez Adebayo
author_url: https://afeezadebayo.com
author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
author_x: https://x.com/za_icon
source: https://afeezadebayo.com/skills-md/frontend-development/css-layout/css-nesting/
category: frontend-development
subcategory: css-layout
version: "1.0.0"
created: "2026-10-30"
updated: "2026-10-30"
---
# CSS Nesting: Write Native Nested CSS That Behaves
Work in this order: lint the stylesheet, decide what is worth nesting, write declarations first and nested rules after, settle the browser support line, then prove a refactor changed no computed style by comparing the old and new sheets in Chromium. Do not trust a nested rewrite until `equiv_check.py` reports 0 differences.
Measured values and source notes are in `references/appendix.md`.
## When to use this skill, and when not to
Use it to convert flat CSS to nested CSS, to write new nested components, or to find out why a nested rule has no effect.
| Request | Use instead |
|---|---|
| Specificity of `:is()`, `:has()` and `:where()` selectors | `modern-css-selectors.md` |
| `@layer` order and which layer wins | `cascade-layers.md` |
| Container query syntax and sizing | `container-queries.md` |
| SCSS, Less or PostCSS configuration and builds | Out of scope. Ask for the compiled CSS |
| Naming convention debates (BEM, utility classes) | Out of scope |
## Guardrails
1. **Propose, then change.** Report and plan first. Edit only after the user approves a batch, on a branch. Never push, merge or deploy.
2. **Bulk edits need confirmation.** Converting more than one stylesheet, or more than 10 rules, is shown as a diff first. Keep the original file until `equiv_check.py` passes.
3. **Data handled.** The tools read CSS and HTML files you pass. `nesting_lint.py` prints selectors and line numbers. `equiv_check.py` prints element paths and property names, never text, field values or storage. Open only pages the user owns, with test data. Never request session cookies or tokens.
4. **Nothing is invented.** Every rule carries a source or a measurement. Version-dependent behaviour carries **verify before relying on this**.
5. **Not a conformance statement.** A clean run means the checked page renders the same in Chromium. Safari and Firefox need their own check.
6. **Refuses to:** nest SCSS syntax as if it were CSS, convert a file that has no equivalence check available without telling the user, claim browser support from memory, or fix a nesting problem by adding `!important`.
## Inputs the agent must have before starting
Ask for every missing item. Do not assume.
| Input | Ask exactly |
|---|---|
| Files | "List the stylesheets, in load order. Are any of them compiled from Sass or Less?" |
| Goal | "Convert existing CSS, write new nested CSS, or fix a rule that does not apply?" |
| Browsers | "Which browsers and minimum versions must work? Is there any engine older than Chrome 120, Safari 17.2 or Firefox 117?" |
| Markup | "Give me a page of markup I may open locally, without the stylesheets under test linked in it." |
| Depth | "Is a nesting depth limit of 3 acceptable, or is there a house limit?" |
| Tooling | "Is Python 3 installed? Is Playwright with Chromium installed for the equivalence check?" |
| Approval | "May I edit on a branch in batches and show each diff first?" |
## Workflow
### Step 0: Confirm scope
If the source is Sass or Less, stop and ask for the compiled output or confirm a one-time migration. Native nesting is not a drop-in for preprocessors: see step 3.
### Step 1: Run the linter
```
python3 scripts/nesting_lint.py styles.css | head -40
```
Read only its output, not the script. The first line has the counts. `--legacy` also flags syntax that needs Chrome 120, Safari 17.2 or Firefox 117. `--max-depth N` changes the depth limit (default 3). `--strict` exits 1 on warnings.
| Code | Level | Meaning | Fix |
|---|---|---|---|
| `scss-syntax` | error | `//` comments, `$vars` or `@mixin` in a `.css` file | Step 0 |
| `amp-ident` | error | `&` directly followed by a name, such as `&__title` or `&-open` | Step 3, rule 4 |
| `amp-pseudo-element` | error | `&` nested under a pseudo-element parent | Step 3, rule 6 |
| `decl-after-rule` | warn | A declaration after a nested rule | Step 3, rule 1 |
| `bare-type` | warn (`--legacy`) | A nested type selector with no `&` | Step 3, rule 3 |
| `id-list-amp` | warn | A parent list containing an id, with `&` in a child | Step 3, rule 5 |
| `depth` | warn | Nesting deeper than the limit | Step 2 |
| `amp-top-level` | info | `&` outside any parent | Delete it or add the parent |
The linter does not run scripts or resolve the cascade. Step 5 covers behaviour.
### Step 2: Decide what to nest
| Situation | Decision |
|---|---|
| Two or more rules share the same parent compound selector (`.card`, `.card:hover`, `.card .title`) | Nest them under it |
| A state of the component (`:hover`, `[aria-expanded="true"]`, `.is-open`) | Nest with `&` |
| The component changes under a parent context (`.dark .card`) | Nest as `.dark & { }` inside `.card` |
| A media or container rule changes only this component | Nest the at-rule inside the component |
| A selector reaches into another component | Do not nest. Keep it flat |
| Depth would exceed 3 | Flatten the inner part with a class. The 3 is a house limit, not a spec rule: the spec sets none (Tiffany Brown, 2023) |
| BEM elements written as `&__title` | Write `.card__title` flat, or `.card { .card__title { } }` if the author wants grouping |
### Step 3: Write the rules
1. **Declarations first, then nested rules.** Before Chrome 130, a declaration placed after a nested rule was moved up and could lose to the nested rule at equal specificity. The fix shipped in Chrome and Edge 130, Safari 18.2 and Firefox 132 (web.dev, 8 October 2024). Putting declarations above nested rules works in every nesting-capable browser. **Verify before relying on this** for the version numbers.
2. **`&` placement.** `&` stands for the parent selector. `&:hover`, `&.is-open`, `& > p`, `.dark &` and `& + &` are valid. It can appear more than once and anywhere in the selector.
3. **Type selectors.** Nested `p { }` with no `&` works from Chrome 120, Safari 17.2 and Firefox 117 (Chrome for Developers, updated 9 November 2023). Older engines need `& p`. Write `& p` unless the support line from Inputs clears all three versions.
4. **No suffix joining.** `&__title` and `&-open` do not join strings as Sass does. Measured in Chromium 153: the rule was ignored and the element kept its inherited colour. The following declaration in the same block still applied, but Tiffany Brown (2023) reported that an invalid nested rule could stop later siblings in older builds, so fix every `amp-ident` row.
5. **Lists gain weight.** A nested `&` behaves like `:is(parent list)`. The CSS Working Group states it as: `.foo, #bar { & { color: green } }` gets id specificity even when only `.foo` matches (CSSWG issue 8738). Measured in Chromium 153: for `.d, #dd { & { color: red } }` followed by `.d { color: green }`, an element with class `d` and no id turned red. Split the list or drop the id. Use `modern-css-selectors.md` to compute the weight.
6. **Pseudo-element parents.** Brown (2023) reports that `&:hover` under `::before` is ignored. **Verify before relying on this** in your support line. Put the state on the real element.
7. **At-rules.** `@media`, `@supports`, `@container` and `@layer` nest inside a rule and apply as if following the parent (Brown). Measured: a nested `@media` containing bare declarations changed `padding` correctly. A nested rule inside `@layer` still lost to an unlayered rule at lower specificity: see `cascade-layers.md`.
8. **Keep nested depth low.** Each level adds the parent's weight. `.a { .b { .c { } } }` weighs the same as `.a .b .c`.
### Step 4: Fix the support line, not the CSS
If every browser in Inputs clears Chrome 120, Safari 17.2 and Firefox 117, ship native nesting. If any does not, do not hand-write fallbacks: Brown measured that an `@supports selector(&)` fallback roughly doubles the CSS sent. Tell the user the choice is to raise the support line or compile nesting out with their build tool, and stop.
### Step 5: Prove equivalence
For a flat to nested refactor, keep the old sheet and run:
```
python3 scripts/equiv_check.py page.html old.css new.css
```
`page.html` is markup only. The script loads it twice, once per sheet, compares every element's computed style and prints the element count, the number that differ and up to 20 paths with property names. Exit 0 means no difference.
| Output | Action |
|---|---|
| `0 differ` | Done for this page |
| Differences with `font-`, `color` or `margin` properties on the elements you edited | A rule was dropped or reordered. Run step 1 again and read the `amp-ident` and `decl-after-rule` rows |
| Differences on the parent only in `height`, `block-size`, `perspective-origin` or `transform-origin` | A child changed size, so the parent derived values changed. Fix the child first |
| Differences on elements you did not edit | A selector got broader or heavier. Check `id-list-amp` and the specificity of the rewritten rule |
A page that never triggers hover, focus or a media query cannot prove those rules. List them as untested, or ask the user for markup states, such as a class that forces the state.
### Step 6: Report
Run the linter again. The error count must be 0. Report a list of file, line, code and fix. Do not echo unchanged CSS. List what was not tested: Safari, Firefox, hover and focus states, print, and breakpoints beyond the one used.
## Worked example
An invented card stylesheet, written in Sass habit:
```css
.card {
padding: 1rem;
border: 1px solid #ccc;
&__title { font-size: 1.25rem; margin: 0; }
&__body { color: #333; }
&__link:hover { text-decoration: underline; }
@media (min-width: 40em) { padding: 2rem; }
}
```
```
$ python3 scripts/nesting_lint.py nested.css
nesting_lint: 3 error, 0 warn, 0 info in 1 file(s)
nested.css:4 error amp-ident | &__title | '&' directly followed by a name is not CSS ...
nested.css:5 error amp-ident | &__body | ...
nested.css:6 error amp-ident | &__link:hover | ...
$ python3 scripts/equiv_check.py page.html flat.css nested.css
equiv_check: 8 elements compared, 3 differ
html>body:nth-child(2)>div#c1 | block-size, height, perspective-origin, transform-origin
html>body:nth-child(2)>div#c1>h2#t1 | block-size, font-size, height, margin-block-end, margin-block-start, margin-bottom +3
html>body:nth-child(2)>div#c1>p#b1 | border-block-end-color, border-block-start-color, ...
```
The title and body lost their rules, and the card's own size changed as a result. Batch one, approved by the user, keeps the nesting for the parent state and flattens the BEM children:
```css
.card {
padding: 1rem;
border: 1px solid #ccc;
@media (min-width: 40em) { padding: 2rem; }
.card__title { font-size: 1.25rem; margin: 0; }
}
.card__body { color: #333; }
.card__link:hover { text-decoration: underline; }
```
```
$ python3 scripts/equiv_check.py page.html flat.css nested.css
equiv_check: 8 elements compared, 0 differ
```
The check ran at Playwright's default 1280 px viewport with no hover state, so the narrow-viewport padding and the `:hover` rule are untested and go in the report.
## Templates
**Report format**
```
css-nesting report: <files>, <date>
Lint: <errors> error, <warn> warn. Support line: <browsers>.
1. <file>:<line> <code> -> <fix applied or proposed>
Equivalence: <n> elements, <m> differ, viewport <w>px.
Not tested: Safari, Firefox, <states>, <breakpoints>
Needs approval: <batch list>
```
**Starting prompt**
```
Use css-nesting. Files: <paths>, load order <order>. Goal: <convert|write|fix>.
Browsers: <list>. Markup for testing: <path>, no stylesheets linked.
Lint first, show findings, then propose a batch. Do not edit until I approve.
```
## Failure modes and anti-patterns
| Failure | How to spot it | Do instead |
|---|---|---|
| Sass-style `&__x` or `&-x` | `amp-ident`; the element is unstyled | Write the full selector |
| Declaration after a nested rule | `decl-after-rule`; the nested rule wins in older engines | Move declarations above nested rules |
| Id in a parent list with a nested `&` | `id-list-amp`; the nested rule overrides everything | Split the list, drop the id |
| Nesting five levels deep | `depth`; overrides need more weight each time | Flatten with a class |
| Assuming old limits still apply, or new features everywhere | The 2023 articles say bare type selectors are invalid; the 2023 Chrome article says they work from 120 | Check the support line in Inputs, then use step 3, rule 3 |
## Quality bar
Run every item. Each is a command or a count.
1. `nesting_lint.py` on all touched files prints `0 error`.
2. No `decl-after-rule` warning remains.
3. No `amp-ident` or `amp-pseudo-element` row remains.
4. For a refactor, `equiv_check.py` prints `0 differ`, or each difference is named and accepted.
5. The report lists untested states and viewports.
6. Every support claim carries a source and a date or a measurement.
7. No file was edited without an approved diff, and no push, merge or deploy happened.
8. The report is a list of file, line, code and fix, with no unchanged CSS repeated.
## Cost
Estimates from the skill's own count (words times 1.35 for prose, characters divided by 3.5 for code and tables), not a model tokeniser.
| Item | Tokens | Read when |
|---|---|---|
| Description | 99 | Always loaded |
| This file | 3,640 | The skill fires |
| `references/appendix.md` | 782 | The user asks for measured values or source notes |
| Script output | 350 | Each run |
| Typical run | 4,089 | Description, this file, script output |
| Worst case | 6,971 | Everything above plus 20 lines of `equiv_check.py` output |
Cheaper path: run `nesting_lint.py`, fix only flagged lines, skip the appendix and the equivalence check on new code.
## Tool-specific appendix
`equiv_check.py` uses Playwright for Python with Chromium. It compares computed styles at Playwright's default 1280 by 720 viewport, so pass markup that forces the state you need. For older engines, compile nesting out with the project's build tool, such as PostCSS with a nesting plugin; this skill does not configure builds.
## Sources
Last verified 30 October 2026.
- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Chrome for Developers, CSS nesting relaxed syntax update: https://developer.chrome.com/blog/css-nesting-relaxed-syntax-update
- web.dev, CSS nesting improves with CSSNestedDeclarations: https://web.dev/blog/css-nesting-cssnesteddeclarations
- Tiffany B. Brown, Native CSS nesting: https://tiffanybbrown.com/2023/04/native-css-nesting/
- CSS Working Group issue 8738 discussion: https://lists.w3.org/Archives/Public/public-css-archive/2023Oct/0570.html
Use, adapt and share under CC BY 4.0: credit Afeez Adebayo, link https://afeezadebayo.com and https://www.linkedin.com/in/afeez-adebayo/. Download URL: https://afeezadebayo.com/wp-content/uploads/skills-md/frontend-development/css-layout/css-nesting/SKILL.md
Workflow
Step 0: Confirm scope
If the source is Sass or Less, stop and ask for the compiled output or confirm a one-time migration. Native nesting is not a drop-in for preprocessors: see step 3.
Step 1: Run the linter
python3 scripts/nesting_lint.py styles.css | head -40Read only its output, not the script. The first line has the counts. --legacy also flags syntax that needs Chrome 120, Safari 17.2 or Firefox 117. --max-depth N changes the depth limit (default 3). --strict exits 1 on warnings.
| Code | Level | Meaning | Fix |
|---|---|---|---|
scss-syntax | error | // comments, $vars or @mixin in a .css file | Step 0 |
amp-ident | error | & directly followed by a name, such as &__title or &-open | Step 3, rule 4 |
amp-pseudo-element | error | & nested under a pseudo-element parent | Step 3, rule 6 |
decl-after-rule | warn | A declaration after a nested rule | Step 3, rule 1 |
bare-type | warn (--legacy) | A nested type selector with no & | Step 3, rule 3 |
id-list-amp | warn | A parent list containing an id, with & in a child | Step 3, rule 5 |
depth | warn | Nesting deeper than the limit | Step 2 |
amp-top-level | info | & outside any parent | Delete it or add the parent |
The linter does not run scripts or resolve the cascade. Step 5 covers behaviour.
Step 2: Decide what to nest
| Situation | Decision |
|---|---|
Two or more rules share the same parent compound selector (.card, .card:hover, .card .title) | Nest them under it |
A state of the component (:hover, [aria-expanded="true"], .is-open) | Nest with & |
The component changes under a parent context (.dark .card) | Nest as .dark & { } inside .card |
| A media or container rule changes only this component | Nest the at-rule inside the component |
| A selector reaches into another component | Do not nest. Keep it flat |
| Depth would exceed 3 | Flatten the inner part with a class. The 3 is a house limit, not a spec rule: the spec sets none (Tiffany Brown, 2023) |
BEM elements written as &__title | Write .card__title flat, or .card { .card__title { } } if the author wants grouping |
Step 3: Write the rules
- Declarations first, then nested rules. Before Chrome 130, a declaration placed after a nested rule was moved up and could lose to the nested rule at equal specificity. The fix shipped in Chrome and Edge 130, Safari 18.2 and Firefox 132 (web.dev, 8 October 2024). Putting declarations above nested rules works in every nesting-capable browser. Verify before relying on this for the version numbers.
&placement.&stands for the parent selector.&:hover,&.is-open,& > p,.dark &and& + &are valid. It can appear more than once and anywhere in the selector.- Type selectors. Nested
p { }with no&works from Chrome 120, Safari 17.2 and Firefox 117 (Chrome for Developers, updated 9 November 2023). Older engines need& p. Write& punless the support line from Inputs clears all three versions. - No suffix joining.
&__titleand&-opendo not join strings as Sass does. Measured in Chromium 153: the rule was ignored and the element kept its inherited colour. The following declaration in the same block still applied, but Tiffany Brown (2023) reported that an invalid nested rule could stop later siblings in older builds, so fix everyamp-identrow. - Lists gain weight. A nested
&behaves like:is(parent list). The CSS Working Group states it as:.foo, #bar { & { color: green } }gets id specificity even when only.foomatches (CSSWG issue 8738). Measured in Chromium 153: for.d, #dd { & { color: red } }followed by.d { color: green }, an element with classdand no id turned red. Split the list or drop the id. Usemodern-css-selectors.mdto compute the weight. - Pseudo-element parents. Brown (2023) reports that
&:hoverunder::beforeis ignored. Verify before relying on this in your support line. Put the state on the real element. - At-rules.
@media,@supports,@containerand@layernest inside a rule and apply as if following the parent (Brown). Measured: a nested@mediacontaining bare declarations changedpaddingcorrectly. A nested rule inside@layerstill lost to an unlayered rule at lower specificity: seecascade-layers.md. - Keep nested depth low. Each level adds the parent's weight.
.a { .b { .c { } } }weighs the same as.a .b .c.
Step 4: Fix the support line, not the CSS
If every browser in Inputs clears Chrome 120, Safari 17.2 and Firefox 117, ship native nesting. If any does not, do not hand-write fallbacks: Brown measured that an @supports selector(&) fallback roughly doubles the CSS sent. Tell the user the choice is to raise the support line or compile nesting out with their build tool, and stop.
Step 5: Prove equivalence
For a flat to nested refactor, keep the old sheet and run:
python3 scripts/equiv_check.py page.html old.css new.csspage.html is markup only. The script loads it twice, once per sheet, compares every element's computed style and prints the element count, the number that differ and up to 20 paths with property names. Exit 0 means no difference.
| Output | Action |
|---|---|
0 differ | Done for this page |
Differences with font-, color or margin properties on the elements you edited | A rule was dropped or reordered. Run step 1 again and read the amp-ident and decl-after-rule rows |
Differences on the parent only in height, block-size, perspective-origin or transform-origin | A child changed size, so the parent derived values changed. Fix the child first |
| Differences on elements you did not edit | A selector got broader or heavier. Check id-list-amp and the specificity of the rewritten rule |
A page that never triggers hover, focus or a media query cannot prove those rules. List them as untested, or ask the user for markup states, such as a class that forces the state.
Step 6: Report
Run the linter again. The error count must be 0. Report a list of file, line, code and fix. Do not echo unchanged CSS. List what was not tested: Safari, Firefox, hover and focus states, print, and breakpoints beyond the one used.
Worked example
An invented card stylesheet, written in Sass habit:
.card {
padding: 1rem;
border: 1px solid #ccc;
&__title { font-size: 1.25rem; margin: 0; }
&__body { color: #333; }
&__link:hover { text-decoration: underline; }
@media (min-width: 40em) { padding: 2rem; }
}$ python3 scripts/nesting_lint.py nested.css
nesting_lint: 3 error, 0 warn, 0 info in 1 file(s)
nested.css:4 error amp-ident | &__title | '&' directly followed by a name is not CSS ...
nested.css:5 error amp-ident | &__body | ...
nested.css:6 error amp-ident | &__link:hover | ...
$ python3 scripts/equiv_check.py page.html flat.css nested.css
equiv_check: 8 elements compared, 3 differ
html>body:nth-child(2)>div#c1 | block-size, height, perspective-origin, transform-origin
html>body:nth-child(2)>div#c1>h2#t1 | block-size, font-size, height, margin-block-end, margin-block-start, margin-bottom +3
html>body:nth-child(2)>div#c1>p#b1 | border-block-end-color, border-block-start-color, ...The title and body lost their rules, and the card's own size changed as a result. Batch one, approved by the user, keeps the nesting for the parent state and flattens the BEM children:
.card {
padding: 1rem;
border: 1px solid #ccc;
@media (min-width: 40em) { padding: 2rem; }
.card__title { font-size: 1.25rem; margin: 0; }
}
.card__body { color: #333; }
.card__link:hover { text-decoration: underline; }$ python3 scripts/equiv_check.py page.html flat.css nested.css
equiv_check: 8 elements compared, 0 differThe check ran at Playwright's default 1280 px viewport with no hover state, so the narrow-viewport padding and the :hover rule are untested and go in the report.
Templates
Report format
css-nesting report: <files>, <date>
Lint: <errors> error, <warn> warn. Support line: <browsers>.
1. <file>:<line> <code> -> <fix applied or proposed>
Equivalence: <n> elements, <m> differ, viewport <w>px.
Not tested: Safari, Firefox, <states>, <breakpoints>
Needs approval: <batch list>Starting prompt
Use css-nesting. Files: <paths>, load order <order>. Goal: <convert|write|fix>.
Browsers: <list>. Markup for testing: <path>, no stylesheets linked.
Lint first, show findings, then propose a batch. Do not edit until I approve.Failure modes and anti-patterns
| Failure | How to spot it | Do instead |
|---|---|---|
Sass-style &__x or &-x | amp-ident; the element is unstyled | Write the full selector |
| Declaration after a nested rule | decl-after-rule; the nested rule wins in older engines | Move declarations above nested rules |
Id in a parent list with a nested & | id-list-amp; the nested rule overrides everything | Split the list, drop the id |
| Nesting five levels deep | depth; overrides need more weight each time | Flatten with a class |
| Assuming old limits still apply, or new features everywhere | The 2023 articles say bare type selectors are invalid; the 2023 Chrome article says they work from 120 | Check the support line in Inputs, then use step 3, rule 3 |
Quality bar
Run every item. Each is a command or a count.
nesting_lint.pyon all touched files prints0 error.- No
decl-after-rulewarning remains. - No
amp-identoramp-pseudo-elementrow remains. - For a refactor,
equiv_check.pyprints0 differ, or each difference is named and accepted. - The report lists untested states and viewports.
- Every support claim carries a source and a date or a measurement.
- No file was edited without an approved diff, and no push, merge or deploy happened.
- The report is a list of file, line, code and fix, with no unchanged CSS repeated.
Cost
Estimates from the skill's own count (words times 1.35 for prose, characters divided by 3.5 for code and tables), not a model tokeniser.
| Item | Tokens | Read when |
|---|---|---|
| Description | 99 | Always loaded |
| This file | 3,640 | The skill fires |
references/appendix.md | 782 | The user asks for measured values or source notes |
| Script output | 350 | Each run |
| Typical run | 4,089 | Description, this file, script output |
| Worst case | 6,971 | Everything above plus 20 lines of equiv_check.py output |
Cheaper path: run nesting_lint.py, fix only flagged lines, skip the appendix and the equivalence check on new code.
Tool-specific appendix
equiv_check.py uses Playwright for Python with Chromium. It compares computed styles at Playwright's default 1280 by 720 viewport, so pass markup that forces the state you need. For older engines, compile nesting out with the project's build tool, such as PostCSS with a nesting plugin; this skill does not configure builds.
Sources
Last verified 30 October 2026.
- Anthropic, Skill authoring best practices: https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- Chrome for Developers, CSS nesting relaxed syntax update: https://developer.chrome.com/blog/css-nesting-relaxed-syntax-update
- web.dev, CSS nesting improves with CSSNestedDeclarations: https://web.dev/blog/css-nesting-cssnesteddeclarations
- Tiffany B. Brown, Native CSS nesting: https://tiffanybbrown.com/2023/04/native-css-nesting/
- CSS Working Group issue 8738 discussion: https://lists.w3.org/Archives/Public/public-css-archive/2023Oct/0570.html
Use, adapt and share under CC BY 4.0: credit Afeez Adebayo, link https://afeezadebayo.com and https://www.linkedin.com/in/afeez-adebayo/. Download URL: https://afeezadebayo.com/wp-content/uploads/skills-md/frontend-development/css-layout/css-nesting/SKILL.md
What this skill does
Native CSS nesting looks like Sass and fails like it does not. In Chromium 153, .card { &__title { font-size: 1.25rem; } } was ignored and the title kept its inherited style, with no error. The same file passes a quick glance, so the fault surfaces on the page, not in the editor.
The skill runs scripts/nesting_lint.py first. It reports & joined to a name, & under a pseudo-element parent, declarations placed after a nested rule, bare type selectors that need Chrome 120, Safari 17.2 or Firefox 117, parent lists that contain an id, and nesting deeper than a limit you set. Before Chrome 130, a declaration after a nested rule was moved up and could lose to the nested rule, according to web.dev. The fix is to put declarations first, which works in every browser that supports nesting.
For a refactor from flat CSS, scripts/equiv_check.py loads your markup twice in Chromium, once with each stylesheet, and compares the computed style of every element. It prints the element count, how many differ and the property names, never page text.
Who it is for
Use it if you are moving a stylesheet from Sass to native CSS, writing components with nesting for the first time, or reviewing nested CSS an AI tool generated. It fits when a nested rule has no effect, when a nested override beats everything, or when you need proof that a refactor changed nothing.
What is inside
The file is 2,529 words (version 1.0.0), with two scripts and one reference file. It holds:
- Six guardrails, including keeping the original sheet until the equivalence check passes and never adding
!importantto fix a nesting problem. - Seven inputs the agent must have first, with the exact question for each, including the browser list and a markup page to test.
- A workflow of steps 0 to 6, a table that maps each linter code to a fix and a decision table for what is worth nesting.
scripts/nesting_lint.py, a standard-library linter with eight codes, andscripts/equiv_check.py, a Playwright comparison that prints paths and property names only.- A worked example with real output (3 errors and 3 differing elements before, 0 after), five failure modes and an eight-item quality bar.
references/appendix.mdwith 13 measured behaviours from Chromium 153 and the notes behind each source.
The token costs below are estimates from the skill’s own count: words times 1.35 for prose and characters divided by 3.5 for code and tables. They are not from a model tokeniser.
| Item | Tokens | Status |
|---|---|---|
| Description (always loaded) | 99 | Estimate |
| SKILL.md body (when the skill fires) | 3,640 | Estimate |
references/appendix.md, read only for measured values or source notes | 782 | Estimate |
| Linter output, typical run | 350 | Estimate |
| Typical run (description, body and script output) | 4,089 | Estimate |
| Cheaper path: same as the typical run, which already skips every reference | 4,089 | Estimate |
| Worst case (appendix, 40 lines of linter output and 20 lines of equivalence output) | 6,971 | Estimate |
How to install and use it in Claude, Cursor and other agents
Claude. Keep the folder as css-nesting/ with SKILL.md, scripts/ and references/ inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/, or in .claude/skills/ inside a project.
Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the scripts beside your code. The linter needs Python 3 only. The equivalence check needs Playwright for Python with Chromium. Then send the starting prompt from the Templates section of the skill.
Before you install. Read the scripts first. The linter reads files and writes nothing. The equivalence check opens markup you provide and prints element paths and property names. The skill tells the agent to edit only in batches you approve.
A worked example
An invented card stylesheet uses &__title, &__body and &__link:hover under .card, with a nested @media that changes the padding. The linter reports 3 errors, all amp-ident. The equivalence check against the flat original reports 8 elements compared and 3 differing: the title and body lost their rules, and the card’s own size changed because its children did.
One approved batch keeps the parent’s declarations and nested @media together, nests .card__title as a plain class, and moves the body and link rules back to flat selectors. The check then reports 0 differences. It ran at Playwright’s default 1280 px viewport with no hover state, so the narrow-viewport padding and the hover rule go into the report as untested.
Common mistakes
The skill is built around five ways this goes wrong:
- Sass-style
&__xor&-x. The rule is dropped silently. The skill writes the full selector. - Declarations after nested rules. Older engines move them up and the nested rule wins. The skill puts declarations first.
- An id in a parent list. A nested
&behaves like:is()over the list, so one id gives every match id weight. The skill splits the list. - Nesting five levels deep. Each level adds the parent’s weight. The skill flattens beyond three, a house limit you can change.
- Trusting old support notes. Articles from 2023 say bare type selectors are invalid, and Chrome’s later update says they work from version 120. The skill asks for your browser list and uses the version numbers cited in the file.
Related skills
Modern CSS Selectors covers the specificity of :is(), :has() and :where() that nested lists rely on. Cascade Layers decides which layer wins for nested rules inside @layer. Container Queries covers the @container rules you may nest inside a component.
Frequently asked questions
Does native CSS nesting need the & symbol?
Not for type selectors any more. Chrome for Developers says bare type selectors nest without & from Chrome 120, and lists Edge 120, Firefox 117 and Safari 17.2 for nesting. Older engines need & p. The skill writes & p unless your browser list clears all three versions, and marks the version numbers to verify.
Why does &__title not work in CSS nesting?
Native CSS does not join strings the way Sass does. Chromium 153 ignored .card { &__title { } } and the element stayed unstyled. The fix is the full selector, such as .card__title or .card .card__title.
Why is my declaration after a nested rule being overridden?
Before Chrome 130, web.dev explains, declarations after a nested rule were moved up and could lose to the nested rule at equal specificity. Chrome and Edge 130, Safari 18.2 and Firefox 132 keep source order. Put declarations above nested rules and it works everywhere nesting works.
Does CSS nesting increase specificity?
Nesting alone does not: & p weighs the same as .card p. Depth and lists do. A nested & acts like :is() over its parent list, so .foo, #bar { & { } } gets id weight even when only .foo matches, as the CSS Working Group discussion records. Chromium 153 reproduced it.
How deep should I nest CSS?
The specification sets no limit, and Tiffany Brown’s 2023 notes say only that deeper nesting makes selectors more specific. The skill uses three levels as a house limit, reports anything deeper as a warning and lets you change it with --max-depth.
Can I nest @media and @container inside a rule?
Yes. @media, @supports, @container and @layer nest inside a rule and apply as if following the parent. Chromium 153 applied a nested @media with bare declarations correctly. A nested rule inside @layer still lost to an unlayered rule at lower specificity, so check layer order too.
How many tokens does the CSS Nesting skill use?
The description is about 99 tokens and loads in every session. The SKILL.md body is about 3,640 tokens when the skill fires. A typical run is about 4,089 tokens with script output, and the worst case with the appendix and long script output is about 6,971. All figures are estimates, not from a model tokeniser.
Changelog
- 1.0.0, 30 October 2026: first release.
Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.
