New CSS that can actually be used in 2024

The amount of CSS novelty in the last two to four years has been staggering. Multiple innovations have been released and are now supported in all modern browsers, and some of them fundamentally change how to make websites.

This page is a cheat sheet for new CSS things I want to start using in 2024. Note that, to support older browsers, it's sometimes a necessity to use polyfills.

If you allow me a little rant The amount of stuff our influencers hype on their websites and newsletters needs to slow down. I know they sincerely like CSS, that their professional life depends on staying up to date and looking relevant in CSS, but damn. When I want to update myself, I have to spend hours checking on Can I Use if the things they talked about in a blog post in 2021 actually exist in my browser ; only to find half of it stuck in technical preview purgatory. It makes me resentful towards people and I don't like that. See also: The continuing tragedy of CSS.

Logical properties

For the longest time many css properties used physical directions to set values. For example : margin-top, padding-right, bottom, border-left. But those properties don't take into account switching to another reading direction, that can be set using direction, text orientation, and writing mode.

For example, a border placed on the left of a text would stay on the left if the text reading direction was reverted, instead of moving to the right to have a similar user experience. Logical properties avoid this issue by allowing setting properties on the X (called inline) and Y (called _block) axis.

I won't go into the whole spec (see MDN) but here are the most common use cases.

For height and width and their variants like max-width and min-width:

For margin, padding, border, and variants:

For inset properties like top, bottom, left, right:

Container queries

Media queries allowed to do an if/else using the browser width or height as a condition. Container size queries allow the same if/else logic, but for any container, not just the browser. You declare a piece of the dom as the container:

.card {
 container-type: size|inline-size|normal;
}

There are three values possible:

size
Allows to use the inline and block dimensions, as well as style queries.
inline-size
Same as above but only for inline dimension (this is the most useful for responsive design).
normal
Deactivate size queries, only let style queries.

And then use it as a media query:

@container (min-width: 700px) {
  .card h2 {
    font-size: 2em;
  }
}

It's also possible to name containers, using the container-name property, or a shortcut width container.

.card {
  container: sidebar / inline-size;
}
@container sidebar (min-width: 700px) {
  .card {
    font-size: 2em;
  }
}

Container size queries also have their own units:

cqw
1% of a query container's width
cqh
1% of a query container's height
cqi
1% of a query container's inline size
cqb
1% of a query container's block size
cqmin
The smaller value of either cqi or cqb
cqmax
The larger value of either cqi or cqb

:has

To understand :has it's important to remember that CSS styling works has a cascade that goes from left to right (even if the browser reads it right to left but that's another topic). The last element on a declaration is the one being styled.

For example, take this HTML where inside main, we select all the article elements, then the strong elements.

<main>
 <article>
  <p>Some text</p>
  <strong>My strong text</strong>
  <p>Some text</p>
 </article>
</main>
main article strong {
 font-weight: 600;
}

But what happens when we want to style main at the condition that strong exists in the HTML? That's the purpose of :has, which will check for us.

main:has(strong) {
 //change main style
}

It's also possible to simulate logical operators likes && and ||. to check multiple conditions, like if main has p and has strong:

main:has(p):has(strong) {
 color:red;
}

If main has p or has strong:

main:has(p, strong) {
 color:red;
}

But that's not all. :has can be combined with CSS selectors like +, ~, > or other pseudo-selectors like nth-child or :not to open new possibilities.

Do a negative check:

main:not(:has(strong)) {
 //change main style
}

Check if strong is after p, if yes change previous p color to red.

p:has(+strong) {
  color:red;
}

Check if strong is after p, if yes change all the previous p color to red.

p:has(~ figure) {
 color:red;
}

For more interactive examples and concrete use cases, see Ahmad Shadeed's article on the topic.

:is and :where

They have the same use: select multiple things in a more concise syntax. For example, chaining of selectors or classes in menus made of lists are a classic issue of specific styling that creates a lof of specificity:

ol ol ul,
ol ul ul,
ul ol ul,
ol, ol, ol,
ul ul ul,{
  list-style-type: square;
}

Can be replaced by :

:is(ol, ul) :is(ol, ul) ul {
   // My style
}
// or
:where(ol, ul) :where(ol, ul) :where(ol, ul) {
  // My style
}

There are more good examples on the MDN page.

The difference between :is and :where is a CSS nerd thing who might, in the long run, make non-CSS specialist throw their keyboard in frustration: is: takes the specificity of the most specific thing it contains, while :where does not.

In a more concrete way, it means something styled inside an :is, if it has a bigger specificity like being a class, an id, or using !important, cannot be changed by a lower specificity rule even if this rule comes down after in the cascade.

For example this HTML:

<main>
  <p class="isClass">Text 1</p>
  <p class="whereClass">Text 2</p>
</main

With this CSS:

:is(.isClass) {
	color:red;
}
:where(.whereClass) {
	color:blue;
}

Looks the same. But what happens if we add this CSS?

main p {
	color: orange;
}

Since main p has a lower specificity than .isClass, it cannot restyle it. Meanwhile, .whereClass has a specificity of 0, and will become orange due to the incoming rule in the cascade.

Nesting

It's quite obvious, it's the nesting from SASS, but without string concatenation (no gluing .myclass with a __children like we do in BEM). Nesting can be used with the nesting selector & or without it for simple use cases.

main {
	article {
		color:red;
	}
}
// Same as :
main {
	& article {
		color:red;
	}
}

The nesting selector is necessary for compound selectors (a combination of element + class like p.myClass) and pseudo classes (:hover, :focus`, etc).

` main { &.myClass { //... } &:hover { //... } } `

CSS Comparison Functions

CSS Comparison Functions allow to compare values to the viewport (or the container if using container queries) and let the browser pick the most appropriate one. Basically a if/else that comes in three ways:

min(a, b)
Takes the smallest value between a and b
max(a, b)
Takes the biggest value between a and b
clamp(a, b, c)
Takes the most appropriate value between smallest a, desired b, and biggest c.

But comparisons can be done with different values than pixels. %, em, rem, vw vh, among others are available. And since math expressions are baked in, it's possible to compare fixed values to live computed values based on the viewport.

For example, for a fluid font who's size will vary when between 16px and 32px, it can be done by summing the viewport width and the relative font size:

font-size: clamp(16px, 1vw + 1rem, 32px);

If you ever struggled adjusting things in media queries with a designer behind you, you understood immediately how revolutionary this is. In a world where designers only produce the mobile and desktop versions of their designs (and sometimes the tablet one), it allows to proportionally automate all the in-between for things like font sizes, margins, spacings, grid and flex gaps.

I have the feeling that this technique hasn't yet got out of the CSS specialist crowd despite being several years old. It may be due to its complexity, the use of frameworks and lack of knowledge in CSS.

If you are interested, visit utopia, which contains easy to use tools to generate the comparison functions for you. You can directly enter the values given by a designer and get the clamp() code as custom properties (aka CSS variables) to maintain your website cohesive.

Cascade layers

Cascade layers allow the creation of multiple CSS cascades and their ordering. The cascade is often the most dreaded aspect of CSS and has led to the creation of multiple methodologies to keep code ordered.

Cascade layers, like the name implies, offers to create separated cascades you can access anywhere in the files, and to layer them in the order we want.

@layer reset base; // Define the order of layers
 
// Put some code inside the reset layer
@layer reset {
 a {
  color: black;
 }
}
 
// Put some code inside the base layer
@layer base {
 a {
  color: grey;
 }
}
 
// Put more code inside the reset layer
@layer reset {
 p {
  color: red;
 }
}

But it doesn't mean that each layer is scoped (@scope exists but is not widely supported yet. In the example above, the color of a will be grey, as the base layer is after the reset layer. This is an organization tool, not a scoping mechanism.

This is especially useful in large code bases. It allows to force the code into a specific order, reduces the stress of looking where to add new code, and if it will affect the rest of the code.

For example, let's say we want to import a third party CSS stylesheet, add it to a layer, and build our own styles on top of it.

// import framework.css and define as framework layer
@import "framework.css" layer(framework);
 
// Order framework before personal
@layer framework personal;
 
@layer personal {
 // My own styles
}

But later in the project, we notice the framework has a buggy style. We can fix it in the framework layer without touching the personal layer. And even better, we don't have to go back at the start of the file:

@import "framework.css" layer(framework);
 
@layer framework personal;
 
@layer personal {
 // My own styles
}
 
 
@layer framework {
 // Small bug fix
}

Last thing: un-layered style always has the priority on layered styles. In the examples above, a style written outside a layer would have erased the previously defined ones. See this very extensive guide to better understand the layering of styles in browsers.

Subgrid

The subgrid property has been long in the waiting and is finally here, and mostly used to fix one very annoying issue. Imagine several columns of the same height, and each of them has the same number of children, but those children don't have the same height.

When looking at the whole, the children are not vertically aligned:

Demonstration of subgrid. Images are not vertically aligned.
When only two children, this issue was fixables by declaring a flex column container and using space-between, but it wasn't ideal. | Full size

Declaring the children as a grid element and subgrid as the value of grid-template-rows allows to align the children:

Demonstration of subgrid. Images are now vertically aligned.
This is a more solid way of doing it, as it allows for more elements having different heights. | Full size
.parent {
 display: grid;
 gap: 1rem;
 grid-template-columns: 1fr 1fr;
}
.children {
 grid-row: span 2;
 display: grid;
 grid-template-rows: subgrid;
}

There is of course more to subgrid than this. Ahmad Shadeed wrote an extensive article on the topic.

Small but noticeable

text-wrap:balance
Allows multiple lines of text to have their lines broken in such a way that each line is roughly the same width. link
Dynamic viewport height or dvh
Takes into account the url bar and UI of browser, unlike vh which did not. link
accent-color
Helps coloring HTML controls that are traditionally cumbersome, like checkboxes, ranges, etc. link
Media Queries Range Syntax
Allows the use of mathematical comparison operators: >, <, >=, or <= in media queries. Ex: @media (width <= 1140px). link
flexbox gap
Same as grid, but now supported in flex. link

Initially published: March 24th, 2024
Generated: March 27th, 2024