Important CSS features web developers should know in 2025

Published on

Things to avoid in JavaScript

In the recent years CSS has received major groundbreaking improvements which open new possibilities and have a significant impact on frontend development. I'll list here one of the most important features.

Nested CSS selectors

One of the relatively recent groundbreaking improvements is the native support for nested CSS selectors. In the past the only way to enjoy nested CSS selectors is to use CSS preprocessors (like SASS, LESS, etc.) which will transpile the code into native CSS. All the main browsers have fully supported this feature since the end of 2023.

So, instead of this:

button { background: red; } button.blue { background: blue; } button span { text-decoration: underline; } div button { border: 3px solid green; }

You can almost confidently write this:

button { background: red; &.blue { background: blue; } span { text-decoration: underline; } div & { border: 3px solid green; } }

The final HTML:

<style> button { background: red; &.blue { background: blue; } span { text-decoration: underline; } div & { border: 3px solid green; } } </style> <button>Button 1</button><br> <button class="blue">Button 2</button><br> <button><span>Button 3</span></button><br> <div> <button><span>Button 4</span></button><br> </div>

Nested CSS selectors not only improve the ergonomics, they can also reduce the CSS size, which reduces the site loading time and network bandwidth.

:has() pseudo-class

In the past there was no way to select an element that contained a certain child or followed by a certain element. You had to handle such cases via JavaScript, server side rendering or just manually. :has() pseudo-class solves this problem and opens up a new world of possibilities:

<style> /* We can select buttons that contain a span element */ button:has(span) { background: red; } /* We can even select buttons that are immediately followed by a span element */ button:has(+ span) { background: orange; } /* Or just followed (not necessarily immediately) by a label element */ button:has(~ label) { background: purple; } </style> <!-- An empty button with no following element, nothing will happen --> <div> <button>Button 1</button> </div> <!-- A button with a span element inside --> <div> <button><span>Button 2</span></button> </div> <!-- Will not work because span doesn't immediately follow button --> <div> <button>Button 3</button> <br> <span></span> </div> <!-- Now it will work --> <div> <button>Button 4</button> <span></span> </div> <!-- A button followed by label --> <div> <button>Button 5</button> <br> <label></label> </div>

@container rules

CSS has supported @media rules for many years. They are the backbone of the adaptive design, where the layout has to adapt and behave differently for different screen resolutions. This mechanism works fine in the vast majority of cases. However it has one major limitation: @media rules only work with the global viewport size, they cannot be applied on an individual container element level.

For example, you have a list element which contains items displayed horizontally. But if the list becomes narrow (less or equal to 250px), the items in the list should be displayed vertically, in order to fit in the list.

If the list(s) occupies the full length of the viewport the @media approach will work fine:

<style> .list { display: flex; justify-content: space-between; gap: 20px; .item { background: red; width: 100%; height: 20px; } @media (max-width: 250px) { flex-direction: column; } } </style> <div class="list"> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> </div> <hr> <div class="list"> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> </div>

However, if the container's width is not equal or proportional to the viewport's width, for example, it's resizable, the @media approach doesn't work:

<style> .list { display: flex; justify-content: space-between; gap: 20px; .item { background: red; width: 100%; height: 20px; } @media (max-width: 250px) { flex-direction: column; } } .horizontally-resizable-elements { display: grid; grid-template-columns: auto 1fr; width: 100%; gap: 0; >* { overflow: hidden; min-width: 15vw; max-width: 80vw; box-sizing: border-box; border: 1px solid black; &:not(:last-child) { resize: horizontal; } } } </style> <div class="horizontally-resizable-elements"> <div class="list"> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> </div> <div class="list"> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> </div> </div>

The @media rule will not respond to the .list size. It will only respond to the viewport size.

Fortunately, @container rules fix this problem. We need to wrap .lists inside .list-containers and register .list-containers as containers. Then we need to change @media to @container:

<style> .list-container { /* This registers the container, so that any element inside can use @container. "inline-size" means the query will be based on the inline dimensions of the container (normally width). */ container-type: inline-size; } .list { display: flex; justify-content: space-between; gap: 20px; .item { background: red; width: 100%; height: 20px; } /* Now the rule will respond to the width of .list-container instead of the viewport width. */ @container (max-width: 250px) { flex-direction: column; } } .horizontally-resizable-elements { display: grid; grid-template-columns: auto 1fr; width: 100%; gap: 0; >* { overflow: hidden; min-width: 15vw; max-width: 80vw; box-sizing: border-box; border: 1px solid black; &:not(:last-child) { resize: horizontal; } } } </style> <div class="horizontally-resizable-elements"> <div class="list-container"> <div class="list"> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> </div> </div> <div class="list-container"> <div class="list"> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> <div class="item"></div> </div> </div> </div>

Containers are supported in all major browsers.

Container units

As a bonus, CSS also supports container units which work similar to viewport relative units (such as vw, vh, etc.), except they are relative to the container's width and height:

<style> .container { container-type: size; resize: both; position: relative; width: 100px; height: 100px; min-width: 20px; min-height: 20px; overflow: hidden; background-color: aqua; >* { position: absolute; display: flex; justify-content: center; align-items: center; &::before { color: white; } } } .qw-qh { &::before { content: "cqw/cqh"; } top: 0; left: 0; width: 30cqi; height: 30cqb; background: red; } .qi-qb { &::before { content: "cqi/qcb"; } bottom: 0; left: 0; width: 30cqi; height: 30cqb; background: green; } .qmin { &::before { content: "cqmin"; } top: 0; right: 0; width: 30cqmin; height: 30cqmin; background: blue; } .qmax { &::before { content: "cqmax"; } bottom: 0; right: 0; width: 30cqmax; height: 30cqmax; background: orange; } </style> <div class="container"> <div class="qw-qh"></div> <div class="qi-qb"></div> <div class="qmin"></div> <div class="qmax"></div> </div>

calc-size()

Many of us who have dealt with transitions between auto size and fixed / max size will remember the pain. The reason is that interpolations with non fixed values, such as auto, don't work as expected. Additionally, using max-width / max-height will make te animations more abrupt. This requires to calculate the element sizes (often with JavaScript).

Fortunately, CSS is starting to support calc-size() and this pain is becoming a thing of the past. However, it's not yet supported in all major browsers. So, don't rely on this in production code, or at least make sure there are fallbacks.

calc-size() accepts 2 parameters:

  1. Calculation size basis. A non fixed size value, such as auto, fit-content, min-content, max-content, etc.
  2. Calculation sum. An expression that uses size, where size is the calculated value of calculation size basis. Works similarly in expressions that are passed to calc().

Here is an example of using calc-size() for menus with max-height: 130px.

<style> .menu-container { position: relative; display: inline-block; } .menu { box-sizing: content-box; position: absolute; left: 0; top: 30px; background: white; max-height: 130px; height: 0; transition: all 1s; width: 130px; overflow-x: hidden; overflow-y: auto; border: 2px solid black; z-index: 10; visibility: hidden; } button:focus+.menu { height: auto; visibility: visible; } label:has(input:checked)~.menu-container button:focus+.menu { height: calc-size(auto, min(size, 130px)); } </style> <label>Use calc-size: <input type="checkbox"></label><br> <div class="menu-container"> <button>Menu with many items</button> <div class="menu"> Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br> Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br> Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br> Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br> Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br> Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br>Item<br> </div> </div> <div class="menu-container"> <button>Menu with few items</button> <div class="menu"> Item<br> Item<br> </div> </div>

Anchor positioning

Sometimes we need to position some elements relative to other elements, for example, when showing a tooltip on hover. In the past this was complicated, it often required to place the positioned element inside the anchor or / and calculate the position via JavaScript. Not even mentioning handling overflowing issues.

In order to solve this CSS introduced anchor positioning.

The most basic usage of anchor positioning can be done by registering the anchor and positioned elements:

/* Registering the anchor element with a unique name */ .anchor-element { anchor-name: --some-unique-anchor; } /* Registering the positioned element which is positioned relative to the anchor element */ /* Here the starting position is the bottom-right corner of the anchor element */ .positioned-element { position-anchor: --some-unique-anchor; position: absolute; bottom: anchor(bottom); right: anchor(bottom); }

Here are some examples:

<style> label:has([name="enableAnchor"]:checked) { ~.anchor:hover { anchor-name: --test-anchor; } ~.tooltip { position-anchor: --test-anchor; label:has([value="rb"]:checked)~& { left: calc(anchor(right) + 10px); top: calc(anchor(bottom) + 10px); } label:has([value="lb"]:checked)~& { left: calc(anchor(left) + 10px); top: calc(anchor(bottom) + 10px); } label:has([value="rt"]:checked)~& { left: calc(anchor(right) + 10px); top: calc(anchor(top) + 10px); } label:has([value="lt"]:checked)~& { left: calc(anchor(left) + 10px); top: calc(anchor(top) + 10px); } } } .tooltip { display: none; position: absolute; background: yellow; padding: 10px; top: 0; left: 0; .anchor:hover~& { display: inline-block; } } label { display: inline-flex !important; gap: 20px; } </style> <label>Enable anchor: <input type="checkbox" name="enableAnchor"></label> <hr> <label> <input type="radio" name="pos" value="rb"> left: calc(anchor(right) + 10px); top: calc(anchor(bottom) + 10px); </label><br> <label> <input type="radio" name="pos" value="lb"> left: calc(anchor(left) + 10px); top: calc(anchor(bottom) + 10px); </label><br> <label> <input type="radio" name="pos" value="rt"> left: calc(anchor(right) + 10px); top: calc(anchor(top) + 10px); </label><br> <label> <input type="radio" name="pos" value="lt"> left: calc(anchor(left) + 10px); top: calc(anchor(top) + 10px); </label><br> <button class="anchor">Anchor button 1</button> <button class="anchor">Anchor button 2</button> <div class="tooltip">Tooltip</div>

Handling overflows with @position-try fallbacks

Anchor positioning is more powerful than just simple positioning. It can even handle situations when the positioned element is overflowing the viewport or the scrolling area.

With the help of the @position-try rule, we can handle overflowing situations:

<style> .anchor { anchor-name: --someAnchor; margin: 100vh 100vw; border: 1px solid black; white-space: nowrap; display: inline-block; padding: 20px; background: white; } @position-try --top-left { position-area: top left; right: calc(anchor(left) + 10px); bottom: calc(anchor(top) + 10px); } @position-try --top-right { position-area: top right; left: calc(anchor(right) + 10px); bottom: calc(anchor(top) + 10px); } @position-try --bottom-left { position-area: bottom left; right: calc(anchor(left) + 10px); top: calc(anchor(bottom) + 10px); } @position-try --bottom-right { position-area: bottom right; left: calc(anchor(right) + 10px); top: calc(anchor(bottom) + 10px); } .tooltip { position: absolute; background: yellow; padding: 10px; position-anchor: --someAnchor; position-try-fallbacks: --top-left, --top-right, --bottom-left, --bottom-right; } </style> <div class="anchor">Anchor</div> <div class="tooltip">Tooltip</div>

The anchor positioning feature is experimental and has limited support. So don't rely on this feature in production code yet.

Conclusion

Here I listed several important features which have a significant impact on frontend development. Nested CSS which improves the CSS code ergonomics and network bandwidth, :has() pseudo-class which allows to select parent or preceding elements, @container rules which extend the adaptive design to container elements, calc-size() which removes the pain when doing animations with non-precalculated sizes and anchor positioning which makes popup / tooltip creation a breeze. These features alone already enhance the developer experience significantly and empower developers to create more efficient, flexible, and visually appealing websites.





Read previous


This site uses cookies. By continuing to use this website, you agree to their use. To find out more, including how to control cookies, see here: Privacy & cookies.