Nesting, :has() and container queries
The three additions that changed how stylesheets are written: rules inside rules, a selector that sees what an element contains, and a component that answers to its own width.
Rules inside rules
A nested rule is written inside another one, and its selector is completed by the rule around it: .card { h2 { … } } means a heading inside a card. The & stands for the parent itself, which is how a rule says &:hover, & h2, or the two-of-the-same kind of selector.
Media queries nest too, so a component's small-screen rules can live beside the rest of it: .card { @media (min-width: 40rem) { … } }. Every evergreen browser has understood nesting since 2023, so a build step is no longer part of the answer.
Nesting is a convenience, and convenience has a limit. Two levels read clearly; four levels mean the selector is describing a structure rather than a component, and a class on the inner element would say the same thing in fewer words. The & is most useful for states and for compound selectors such as &.is-open, where repeating the parent name would be noise.
:has() looks down before it styles up
For most of its life CSS could only travel forwards — choose a child, a sibling, a descendant. :has() reverses one step: article:has(.tag) chooses the article because of what is inside it, and styles the container from the content.
Use it for the state that belongs to the container: a field with an error inside, a card with an image, a list with a selected row. The cost is specificity and support — it weighs like the class it is attached to, and it arrived in browsers through 2023.
A component answers to its own width
A media query asks about the window. A container query asks about the component's own box: give an element container-type: inline-size, and inside it write @container (min-width: 20rem) { … }. The rules apply when that box is wide enough — the viewport is not consulted.
A card in a grid is the honest example: the same card is wide in one layout and narrow in another at the same window size. With a container query the card changes with its slot, which is what makes it reusable in both.
One component, all three ideas
.cards {
container-type: inline-size;
}
.card {
border: 1px solid #d4d8e0;
& h2 { margin: 0 0 0.5rem; }
&:hover { border-color: #0b6b47; }
&:has(.tag) { border-color: #0b6b47; border-width: 2px; }
}
@container (min-width: 20rem) {
.card { padding: 1.5rem 2rem; }
}What nesting does to a selector
A nested rule is shorthand for a longer selector, not a new scope. .card { h2 { … } } means .card h2 — a descendant, at any depth. If you mean the card itself in a state, write &:hover; without the ampersand, :hover alone would mean a descendant of the card being hovered.
Nested at-rules keep their own meaning too. A @media inside .card still asks about the viewport, and a @container inside it asks about the nearest container. Nesting only saves you from repeating the parent selector; it does not change what the query measures.
The cost of looking down
:has() lets a selector travel from a container to its contents, which the language could not do before. The browser has to re-evaluate it when a descendant changes, so it is best used for a small, stable piece of state — a card with a tag, a field with an error — rather than as the main way to style a large list.
A container query needs a container. container-type: inline-size makes an element answer questions about its inline size, and the query then styles what sits inside that box, which is why the common pattern is a wrapper with the type and the component inside it. The viewport is never consulted, so the same component can change in a narrow slot while the window stays wide.
Both features arrived in browsers through 2023, which is recent enough that a fallback is worth a thought. The common pattern is to write the base style first and let the modern selector add to it, so an older browser gets a usable page rather than an unstyled one. A feature query can ask whether the browser understands the rule, but for most sites the simpler answer is to make the base good enough on its own.
Three modern-CSS mistakes: nesting a state as :hover when you meant &:hover, which silently changes the selector to a descendant; writing @container without giving an ancestor container-type, so the query never matches; and reaching for :has() when the element already carries a class that says the same thing — a class is cheaper to match and easier to read.
Try these next
- Convert one
.card h2rule to nesting, then add&:hoverand check the generated selector in the inspector. - Add a wrapper with
container-type: inline-sizeand change a child’s layout at 30rem. - Use
:has()to outline a form field that contains an error message, then compare it with a class-based version.
Cards that know their place
Style the cards the modern way: nest the heading rule inside the card, let a card that contains a tag style itself, and add a container query so the cards answer to their own width.
4 left to do
- The heading rule is nested under the card
- A card that holds a tag is chosen
- The card shelf is a query container
- A container query changes the card
Start with .cards { container-type: inline-size; }, nest the heading as .card { & h2 { … } }, add .card:has(.tag) { … } for the tagged cards, and finish with @container (min-width: 20rem) { .card { padding: … } }.
A panel that answers its slot
The panel sits in a narrow sidebar on a wide screen and across the page on a small one. Give its wrapper a container type, then use a container query so the panel’s heading and text sit side by side only when the panel itself is at least 30rem wide. Nest the heading rule inside the panel.
3 left to do
- The wrapper is a query container
- A container query changes the panel
- The heading rule is nested with the ampersand
.panel-wrap { container-type: inline-size; }, then @container (min-width: 30rem) { .panel { display: flex; gap: 1rem; } }. Nest the heading as .panel { & h2 { … } }.
Nesting is a convenience, not a puzzle. If a rule needs three levels to reach its element, a class on that element is usually the better answer — one level is the common depth, and & is most useful for states that need the parent name.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does the ampersand mean inside a nested rule?
