The best new CSS features for 2023

From little-known scroll-snap properties to astonishing new color palettes, here are 10 Cascading Style Sheets updates you won't want to miss.

Shutterstock / Chamille White

Cascading Style Sheets (CSS) first dropped in 1996, and it remains an essential, evolving part of the web development stack. Like other living languages, CSS is constantly introducing new features in response to real-world practices. This quick evolution can make it easy for even dedicated developers to miss new features. Here’s a look at the most useful new and upcoming features in CSS this year.

Cascade layers

Cascade layers is an elaboration of how CSS specificity and inheritance work. It’s a bit abstract to understand, but also fundamental to CSS. A layer is a kind of logical space that is interjected into the algorithm a CSS engine uses to determine what styles apply to elements based on cascading, specificity, and inheritance.

The core use case for layers is making it easier to combine multiple style sources into a single project. Multiple CSS source libraries can be combined as named themes that don’t conflict internally, making it easier for different teams to collaborate on large and diverse CSS codebases.

The main syntax addition is the @layer rule and its related programmatic equivalents. Within a set of styles declared with @layer, all styles cascade together. The order of layers imported will determine how conflicts are resolved in the consuming page.

The specification for Cascade layers says:

Authors can create layers to represent element defaults, third-party libraries, themes, components, overrides, and other styling concerns—and are able to re-order the cascade of layers in an explicit way, without altering selectors or specificity within each layer, or relying on source-order to resolve conflicts across layers.

As an example, you could define two layer styles, @main and @library. When you imported them, the order of layer import would determine which styling took precedence. Cascade layers makes it possible to resolve competing styles at the layer level, instead of having to go into the main layer and address conflicts at the style level.

The inert attribute

Most browsers (including Chrome, Edge, and Safari) now have support for inert. At the time of this writing, it is slated to be added to Firefox 112. 

The inert attribute is a global attribute that can be used on any element to indicate it is not interactive. This is something like a more generalized disabled attribute. In fact, inert has the same effect when applied (or inherited) by control-like buttons. The inert attribute also makes elements invisible to assistive technology like screen readers.

So what inert gives us is a way to negatively define non-active portions of the user interface, whereas before we had to positively define the active parts. 

The :has pseudo-class

The :has pseudo-class is an interesting addition to the selector aspect of CSS. As of this writing, all the major browsers except Firefox support it, with Firefox allowing you to opt-in by setting layout.css.has-selector.enabled to true. 

As the MDN docs state, :has is “a way of selecting a parent element or a previous sibling element with respect to a reference element by taking a relative selector list as an argument.”

In essence, :has lets you select elements that contain other elements. For example, if you wanted to pick all the <div>s that have a <span> in them, you could use the :has selector as shown in Listing 1.

Listing 1. Using the :has pseudo-class (example 1)


div:has(span)

That’s pretty useful already, as a way to select elements up the selector chain, but there is more. For example, you could select all the <div>s with a paragraph immediately following, as shown in Listing 2.

Listing 2. Using the :has pseudo-class (example 2)


div:has(+ p)

Grid layout and subgrid

Since its inception, developers have complained about certain monstrous shortcomings in CSS. Some commonplace tasks, like centering something, require overly complex workarounds and finagling. Another big issue was getting a reasonable grid layout, at least until the CSS Grid Layout module stepped into the breach.

Grid is now fully supported and standardized on all browsers.

A grid layout is denoted with the display: grid declaration and is a kind of cousin to Flexbox, in that it lets you define rectangular layouts but also control your grid in two dimensions. Research shows that most developers with our hands in CSS are aware of Grid Layout, and many are using it.

The subgrid value is a newer and very helpful feature for the Grid Layout module. subgrid lets you define a child grid that will inherit its parent's layout. This is distinct from nesting a grid display within another. In that case, the child grid defines its own dimensions and gaps. With subgrid, the parent’s layout is applied but the subgrid can still override aspects of the layout if necessary.

At the time of this writing, subgrid is implemented in Firefox 71 or higher and Safari 16 and up. It’s on the roadmap for Google Chrome and Microsoft Edge. The subgrid value is going to be a very helpful layout feature going forward.

The accent-color option

Some display elements are traditionally difficult to style despite being commonly used. Checkboxes and radio buttons, for instance, are often replaced with a custom widget that mimics the behavior of these elements while hiding the browser’s implementation. The new CSS accent-color option allows you to target these elements.

For example, you could apply a magenta color to all the radio buttons on your page, as shown in Listing 3 (also see this live example).

Listing 3. Controlling radio button colors in CSS


<style>
input[type="radio"] {
    accent-color: magenta;
}
</style>

<form action="/foo.bar">
  <p>Select your favorite outdoor adventure type</p>
  <input type="radio" id="mountain" name="type" value="mountain">
  <label for="mountain">Mountain</label><br>
  <input type="radio" id="ocean" name="type" value="ocean">
  <label for="ocean">Ocean</label><br>
  <input type="radio" id="desert" name="type" value="desert">
  <label for="desert">Desert</label>
</form>

Scroll snap

CSS offers a handy set of properties for controlling the scroll-snap action in a web browser. Some parts of this functionality have been in place for a couple of years while others are still being rolled out to more recent browser versions.

What's interesting is that as of 2022, over a third of CSS users still weren't aware of scroll snap.

The scroll-snap-* properties command gives you quite a few ways to fine-tune how the scroll position works on a container. Developers get greater precision and end users get a smoother, more controlled user experience. Listing 4 gives a small example of controlling the scroll snap on a div (also see this live example).

Listing 4. Simple scroll snap example


<style>
  .scroll-container,
  .scroll-area {
    max-width: 850px;
    height: 300px;
    font-size: 60px;
  }

  .scroll-container {
    overflow: auto;
    scroll-snap-type: y mandatory;
    height: 500px;
  }

  .scroll-area {
    scroll-snap-align: start;
  }

  .scroll-container,
  .scroll-area {
    margin: 0 auto;
  }

  .scroll-area {
    display: flex;
    align-items: center;
    justify-content: center;
    color: white;
  }

  .scroll-area:nth-of-type(1) {  background: IndianRed; }
  .scroll-area:nth-of-type(2) {  background: Moccasin; }
  .scroll-area:nth-of-type(3) {  background: thistle; }
  .scroll-area:nth-of-type(4) {  background: seagreen; }
</style>

<div class="scroll-container">
	<div class="scroll-area">1</div>
	<div class="scroll-area">2</div>
	<div class="scroll-area">3</div>
	<div class="scroll-area">4</div>
</div>

No matter where you release the scroll movement, the y scroll position in Listing 4 automatically moves to the child element. This is because the scroll container has the scroll-snap-type property set to y mandatory, and the child elements have the scroll-snap-align: start declaration.

You can also modify this behavior. For example, you could set the scroll-snap-type property to y proximity. That does as you’d expect, and snaps only when the scroll nears the proximity of the element.

As a side note, the related overscroll-behavior property lets you define how nested-scroll containers behave.

CSS Logical Properties (inline and block)

If you’ve ever wanted to set a container border on the left and right, or bottom and top, you’ve experienced the annoyance of having to write out the border-left and border-right, or border-top and border-bottom properties verbatim. The issue is that there's no way to leverage the shortcut property without also affecting the borders you don’t want to manipulate. This inconvenience also applies to elements like padding and margins. 

The CSS Logical Properties module lets you use the inline and block keywords to refer to things in an abstract way. When you want to talk about left and right, use inline; when you want to refer to top and bottom, use block. For example, to set a border on the left and right of a div, you could use the code in Listing 5 (also see a live example here).

Listing 5. Left and right padding with logical inline


div {
  border-inline: 10px dashed seagreen;
}

These are useful shortcuts for borders, but you can also find the inline and block logical keywords in a host of other properties.

Most developers use these shortcuts to deal with text-direction and writing-mode considerations. In these cases, using a property like padding-inline-end lets you target the trailing padding regardless of what text direction is active. Basically, the abstraction to inline and block allows for writing generalized styles that apply to a variety of settings. See CSS Logical Properties and Values for a more in-depth discussion.

Container queries

Container queries are now stabilized in CSS and implemented by all the major browsers. They make a big impact on how we think about responsive design. The basic idea is that you are able to set a breakpoint not just based on viewport and media, but on the size of a parent container.

The syntax is not fully defined, but it will likely be something like Listing 6.

Listing 6. @container


@container (max-width: 650px){ … }

This way, you can fine-tune a layout based on the size of different containers, which are manifested throughout the nested layers of a UI. 

@when and @else

While we're thinking about the new @container query, did you know that conditional @when and @else query support is also on the horizon? It is not yet supported by any of the major browsers but will be coming in the not-too-distant future.

The @when and @else queries enable a conditional if/then-style logic flow when dealing with media and support queries. They will simplify your life in many complex CSS situations and layouts.

Three new color palettes

Since time immemorial, CSS practitioners have used RGB, HEX, and named colors to beautify and enliven their device displays. More recently, the HSL-style color declaration was introduced. Now, the CSS specification is introducing new ways to denote colors; namely, hwb, lch, and lab.

HWB stands for hue, whiteness, and blackness. It's a neat addition that is notable for its human readability—you pick a color and then add white and black. It's supported in recent versions of Chrome, Firefox, and Safari. (The Microsoft Edge feature reference is oddly silent on this topic.) See hwb() – a color notation for humans? to learn more about HWB. Like RGB and HWL, it supports an alpha channel for transparency.

LCH, short for lightness, chroma, and hue, is notable for increasing the range of available colors. LCH colors in CSS: what, why, and how? is a nice overview with an eye-opening discussion of color theory in CSS.

LAB, derived from the CIE LAB color theory, is the most mind-stretchingly theoretical of the new color spaces. The LAB color descriptor purports to encompass the entire range of human-perceptible colors, which is quite a claim. You can learn more about LAB for CSS from the Mozilla CSS documentation.

Both LAB and LCH are supported by all the major browsers except Firefox, which requires the layout.css.more_color_4.enabled switch to be set to true.