Important CSS features web developers should know in 2025
Published on

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 .list
s inside .list-container
s and register
.list-container
s 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:
cqw
: 1% of the container's width.cqh
: 1% of the container's height.cqi
: 1% of the container's inline size.cqb
: 1% of the container's block size.cqmin
: the smallest value of cqi or cqb.cqmax
: the greatest value of cqi or cqb.
<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:
- Calculation size basis. A non fixed size value, such as
auto
,fit-content
,min-content
,max-content
, etc. - Calculation sum. An expression that uses
size
, wheresize
is the calculated value of calculation size basis. Works similarly in expressions that are passed tocalc()
.
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.