State of CSS 2022

Web styling features of today and tomorrow, as seen at Google IO 2022, plus some extras.

The year 2022 is set to be one of CSS's greatest years, in both features and cooperative browser feature releases, with a collaborative goal to implement 14 features!

Overview

This post is the article form of the talk given at Google IO 2022. It's not meant to be an in-depth guide on each feature, rather an introduction and brief overview to pique your interest, providing breadth instead of depth. If your interest is piqued, check the end of a section for resource links to more information.

Table of contents

Use the list below to jump to topics of interest:

Browser compatibility

A primary reason so many CSS features are set to cooperatively release is due to the efforts of Interop 2022. Before studying the Interop efforts, it's important to look at Compat 2021’s efforts.

Compat 2021

The goals for 2021, driven by developer feedback via surveys, were to stabilize current features, improve the test suite and increase passing scores of browsers for five features:

  1. sticky positioning
  2. aspect-ratio sizing
  3. flex layout
  4. grid layout
  5. transform positioning and animation

Test scores were raised across the board, demonstrating upgraded stability and reliability. Big congratulations to the teams here!

Interop 2022

This year, browsers met together to discuss the features and priorities they intended to work on, uniting their efforts. They planned to deliver the following web features for developers:

  1. @layer
  2. Color spaces and functions
  3. Containment
  4. <dialog>
  5. Form compatibility
  6. Scrolling
  7. Subgrid
  8. Typography
  9. Viewport units
  10. Web compat

This is an exciting and ambitious list that I can't wait to see unfold.

Fresh for 2022

Unsurprisingly, the state of CSS 2022 is dramatically impacted by the Interop 2022 work.

Cascade layers

Browser Support

  • Chrome: 99.
  • Edge: 99.
  • Firefox: 97.
  • Safari: 15.4.

Source

Before @layer, the discovered order of loaded stylesheets was very important, as styles loaded last can overwrite previously loaded styles. This led to meticulously managed entry stylesheets, where developers needed to load less important styles first and more important styles later. Entire methodologies exist to assist developers in managing this importance, such as ITCSS.

With @layer, the entry file can pre-define layers, and their order, ahead of time. Then, as styles load, are loaded or defined, they can be placed within a layer, allowing a preservation of style override importance but without the meticulously managed loading orchestration.

The video shows how the defined cascade layers allow for a more liberated and freestyle authoring and loading process, while still maintaining the cascade as needed.

Chrome DevTools is helpful for visualizing which styles are coming from which layers:

Screenshot of the Styles sidebar of Chrome Devtools, highlighting how styles appear within new Layer groups.

Resources

Subgrid

Browser Support

  • Chrome: 117.
  • Edge: 117.
  • Firefox: 71.
  • Safari: 16.

Source

Before subgrid, a grid inside of another grid couldn't align itself to its parent cells or grid lines. Each grid layout was unique. Many designers place a single grid over their whole design and constantly align items within it, which couldn't be done in CSS.

After subgrid, a child of a grid can adopt its parents’ columns or rows as its own, and align itself or children to them!

In the following demo, the body element creates a classic grid of three columns: the middle column is called main, and the left and right columns name their lines fullbleed. Then, each element in the body, <nav> and <main>, adopts the named lines from body by setting grid-template-columns: subgrid.

​​body {
  display: grid;
  grid-template-columns:
    [fullbleed-start]
    auto [main-start] min(90%, 60ch) [main-end] auto
    [fullbleed-end]
  ;
}

body > * {
  display: grid;
  grid-template-columns: subgrid;
}

Lastly, children of <nav> or <main> can align or size themselves using the fullbleed and main columns and lines.

.main-content {
  grid-column: main;
}

.fullbleed {
  grid-column: fullbleed;
}

Devtools can help you see the lines and subgrids (Firefox only at the moment). In the following image, the parent grid and subgrids have been overlaid. It now resembles how designers were thinking about the layout.

Screenshot of a subgrid demo, using the Chrome Devtools grid overlay tooling to show the lines defined by CSS.

In the elements panel of devtools you can see which elements are grids and subgrids, which is very helpful for debugging or validating layout.

Screenshot of the Chrome Devtools Elements panel labelling which elements have grid or subgrid layouts.
Screenshot from Firefox Devtools

Resources

Container queries

Browser Support

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 110.
  • Safari: 16.

Source

Before @container, elements of a webpage could only respond to the size of the whole viewport. This is great for macro layouts, but for micro layouts, where their outer container isn't the whole viewport, it's impossible for the layout to adjust accordingly.

After @container, elements can respond to a parent container size or style! The only caveat is the containers must declare themselves as possible query targets, which is a small requirement for a large benefit.

/* establish a container */
.day {
  container-type: inline-size;
  container-name: calendar-day;
}

These styles are what make the Mon, Tues, Wed, Thurs, and Fri columns in the following video able to be queried by the event elements.

Demo by Una Kravets

Here is the CSS for querying the calendar-day container for its size, then adjusting a layout and font sizes:

@container calendar-day (max-width: 200px) {
  .date {
    display: block;
  }

  .date-num {
    font-size: 2.5rem;
    display: block;
  }
}

Here's another example: one book component adapts itself to the space available in the column that it's dragged to:

Demo by Max Böck

Una is correct in assessing the situation as the new responsive. There are many exciting and meaningful design decisions to make when using @container.

Resources

accent-color

Browser Support

  • Chrome: 93.
  • Edge: 93.
  • Firefox: 92.
  • Safari: 15.4.

Source

Before accent-color, when you wanted a form with brand matching colors, you could end up with complex libraries or CSS solutions that became hard to manage over time. While they gave you all the options, and hopefully included accessibility, the choice to use the built-in components or adopt your own becomes tedious to continue to choose.

After accent-color, one line of CSS brings a brand color to the built-in components. In addition to a tint, the browser intelligently chooses proper contrasting colors for ancillary parts of the component and adapts to system color schemes (light or dark).

/* tint everything */
:root {
  accent-color: hotpink;
}

/* tint one element */
progress {
  accent-color: indigo;
}

Light and dark accented HTML elements side by side for comparison.

To learn more about accent-color, check out my post on web.dev where I explore many more aspects of this useful CSS property.

Resources

Color level 4 and 5

The web has been dominated by sRGB for the past decades, but in an expanding digital world of high-definition displays and mobile devices pre-equipped with OLED or QLED screens, sRGB is not enough. Furthermore, dynamic pages that adapt to user preferences are expected, and color management has been a growing concern for designers, design systems, and code maintainers.

Not in 2022 though—CSS has a number of new color functions and spaces: - Colors that reach into the HD color capabilities of displays. - Color spaces that match an intent, such as perceptual uniformity. - Color spaces for gradients that drastically change the interpolation outcomes. - Color functions to help you mix and contrast, and choose which space you do the work in.

Before all these color features, design systems needed to precalculate proper contrasting colors, and ensure appropriately vibrant palettes, all while preprocessors or JavaScript did the heavy lifting.

After all these color features, the browser and CSS can do all the work, dynamically and just in time. Instead of sending many KBs of CSS and JavaScript to users to enable theming and data visualization colors, CSS can do the orchestrating and calculations. CSS is also better equipped to check for support before usage or handle fallbacks gracefully.

@media (dynamic-range: high) {
  .neon-pink {
    --neon-glow: color(display-p3 1 0 1);
  }
}

@supports (color: lab(0% 0 0)) {
  .neon-pink {
    --neon-glow: lab(150% 160 0);
  }
}

hwb()

Browser Support

  • Chrome: 101.
  • Edge: 101.
  • Firefox: 96.
  • Safari: 15.

Source

HWB stands for hue, whiteness, and blackness. It presents itself as a human-friendly way of articulating color, as it's just a hue and an amount of white or black to lighten or darken. Artists who mix colors with white or black may find themselves appreciating this color syntax addition.

Using this color function results in colors from the sRGB color space, the same as HSL and RGB. In terms of newness for 2022, this doesn’t give you new colors, but it may make some tasks easier for fans of the syntax and mental model.

Resources

Color spaces

The way colors are represented is done with a color space. Each color space offers various features and trade-offs for working with color. Some may pack all the bright colors together; some may line them up first based on their lightness.

2022 CSS is set to offer 10 new color spaces, each with unique features to assist designers and developers in displaying, picking, and mixing colors. Previously, sRGB was the only option for working with color, but now CSS unlocks new potential and a new default color space, LCH.

color-mix()

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 16.2.

Source

Before color-mix(), developers and designers needed preprocessors like Sass to mix the colors before the browser saw them. Most color-mixing functions also didn't provide the option to specify which color space to do the mixing in, sometimes resulting in confusing results.

After color-mix(), developers and designers can mix colors in the browser, alongside all their other styles, without running build processes or including JavaScript. Additionally, they can specify which color space to do the mixing in, or use the default mixing color space of LCH.

Often, a brand color is used as a base and variants are created from it, such as lighter or darker colors for hover styles. Here's what that looks like with color-mix():

.color-mix-example {
  --brand: #0af;

  --darker: color-mix(var(--brand) 25%, black);
  --lighter: color-mix(var(--brand) 25%, white);
}

and if you wanted to mix those colors in a different color space, like srgb, change it:

.color-mix-example {
  --brand: #0af;

  --darker: color-mix(in srgb, var(--brand) 25%, black);
  --lighter: color-mix(in srgb, var(--brand) 25%, white);
}

Here follows a theming demo using color-mix(). Try changing the brand color and watch the theme update:

Enjoy mixing colors in various color spaces in your stylesheets in 2022!

Resources

color-contrast()

Before color-contrast(), stylesheet authors needed to know accessible colors ahead of time. Often a palette would show black or white text on a color swatch, to indicate to a user of the color system which text color would be needed to properly contrast with that swatch.

Screenshot of 3 Material palettes, showing 14 colors and their appropriate white or black contrast colors for text.
Example from 2014 Material Design color palettes

After color-contrast(), stylesheet authors can offload the task entirely to the browser. Not only can you employ the browser to automatically pick a black or white color, you can give it a list of design system appropriate colors and have it pick the first to pass your desired contrast ratio.

Here's a screenshot of an HWB color palette set demo where the text colors are automatically chosen by the browser based on the swatch color:

Screenshot of the HWB demo where each palette has a different pairing of light or dark text, as determined by the browser.
Try the demo

The basics of the syntax look like this, where gray is passed to the function and the browser determines if black or white have the most contrast:

color: color-contrast(gray);

The function can also be customized with a list of colors, from which it will pick the highest contrasting color from the selection:

color: color-contrast(gray vs indigo, rebeccapurple, hotpink);

Lastly, in case it's preferable not to pick the highest contrasting color from the list, a target contrast ratio can be provided, and the first color to pass it is chosen:

color: color-contrast(
  var(--bg-blue-1)
  vs
  var(--text-lightest), var(--text-light), var(--text-subdued)
  to AA /* 4.5 could also be passed */
);

This function can be used for more than just text color, though I estimate that will be its primary use case. Think about how much easier it will be to deliver accessible and legible interfaces once the choosing of proper contrasting colors is built into the CSS language itself.

Resources

Relative color syntax

Browser Support

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 113.
  • Safari: 15.

Source

Before relative color syntax, to compute on color and make adjustments, the color channels needed to be individually placed into custom properties. This limitation also made HSL the primary color function for manipulating colors because the hue, saturation, or lightness could all be adjusted in a straightforward way with calc().

After relative color syntax, any color in any space can be deconstructed, modified, and returned as a color, all in one line of CSS. No more limitations to HSL—manipulations can be done in any color space desired, and many less custom properties need to be created to facilitate it.

In the following syntax example, a base hex is provided and two new colors are created relative to it. The first color --absolute-change creates a new color in LCH from the base color, then proceeds to replace the base color’s lightness with 75%, maintaining the chroma (c) and hue (h). The second color --relative-change creates a new color in LCH from the base color, but this time reduces the chroma (c) by 20%.

.relative-color-syntax {
  --color: #0af;
  --absolute-change: lch(from var(--color) 75% c h);
  --relative-change: lch(from var(--color) l calc(c-20%) h);
}

It's akin to mixing colors, but it's more similar to alterations than it is mixing. You get to cast a color from another color, getting access to the three channel values as named by the color function used, with an opportunity to adjust those channels. All in all, this is a very cool and powerful syntax for color.

In the following demo I've used relative color syntax to create lighter and darker variants of a base color, and used color-contrast() to ensure the labels have proper contrast:

Screenshot with 3 columns, each column is either darker or lighter than the center column.
Try the demo

This function can also be used for color palette generation. Here is a demo where entire palettes are generated off a provided base color. This one set of CSS powers all the various palettes, each palette simply provides a different base. As a bonus, since I've used LCH, look at how perceptually even the palettes are—no hot or dead spots to be seen, thanks to this color space.

:root {
  --_color-base: #339af0;

  --color-0:  lch(from var(--_color-base) 98% 10 h);
  --color-1:  lch(from var(--_color-base) 93% 20 h);
  --color-2:  lch(from var(--_color-base) 85% 40 h);
  --color-3:  lch(from var(--_color-base) 75% 46 h);
  --color-4:  lch(from var(--_color-base) 66% 51 h);
  --color-5:  lch(from var(--_color-base) 61% 52 h);
  --color-6:  lch(from var(--_color-base) 55% 57 h);
  --color-7:  lch(from var(--_color-base) 49% 58 h);
  --color-8:  lch(from var(--_color-base) 43% 55 h);
  --color-9:  lch(from var(--_color-base) 39% 52 h);
  --color-10: lch(from var(--_color-base) 32% 48 h);
  --color-11: lch(from var(--_color-base) 25% 45 h);
  --color-12: lch(from var(--_color-base) 17% 40 h);
  --color-13: lch(from var(--_color-base) 10% 30 h);
  --color-14: lch(from var(--_color-base) 5% 20 h);
  --color-15: lch(from var(--_color-base) 1% 5 h);
}
Screenshot of 15 palettes all generated dynamically by CSS.
Try the demo

Hopefully by now you can see how color spaces and different color functions can all be used for different purposes, based on their strengths and weaknesses.

Resources

Gradient color spaces

Before gradient color spaces, sRGB was the default color space used. sRGB is generally reliable, but does have some weaknesses like the gray dead zone.

4 gradients in a grid, all from cyan to deeppink. LCH and LAB have more consistent vibrancy, where sRGB goes a bit desaturated in the middle.

After gradient color spaces, tell the browser which color space to use for the color interpolation. This gives developers and designers the ability to choose the gradient they prefer. The default color space also changes to LCH instead of sRGB.

The syntax addition goes after the gradient direction, uses the new in syntax, and is optional:

background-image: linear-gradient(
  to right in hsl,
  black, white
);

background-image: linear-gradient(
  to right in lch,
  black, white
);

Here's a basic and essential gradient from black to white. Look at the range of results in each color space. Some reach dark black earlier than others, some fade to white too late.

11 color spaces shown comparing black to white.

In this next example, black is transitioned to blue because it's a known problem space for gradients. Most color spaces creep into purple during color interpolation or, as I like to think of it, as colors travel inside their color space from point A to point B. Since the gradient will take a straight line from point A to point B, the shape of the color space drastically changes the stops that the path takes along the way.

11 color spaces shown comparing blue to black.

For more deep explorations, examples and comments, read this Twitter thread.

Resources

inert

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5.

Source

Before inert, it was good practice to guide the user's focus to areas of the page or app that needed immediate attention. This guided focus strategy became known as focus trapping because developers would place focus into an interactive space, listen for focus change events and, if the focus left the interactive space, then it was forced back in. Users on keyboards or screen readers are guided back to the interactive space to ensure the task is complete before moving on.

After inert, no trapping is required because you can freeze or guard entire sections of the page or app. Clicks and focus change attempts are just simply not available while those parts of a document are inert. One could also think of this like guards instead of a trap, where inert is not interested in making you stay somewhere, rather making other places unavailable.

A good example of this is the JavaScript alert() function:

Website is shown as interactive, then an alert() is called, and the page is no longer active.

Notice in the preceding video how the page was mouse and keyboard accessible until an alert() was called. Once the alert dialog popup was shown, the rest of the page was frozen, or inert. Users’ focus is placed inside the alert dialog and has nowhere else to go. Once the user interacts and completes the alert function request, the page is interactive again. inert empowers developers to achieve this same guided focus experience with ease.

Here's a small code sample to show how it works:

<body>
  <div class="modal">
    <h2>Modal Title</h2>
    <p>...<p>
    <button>Save</button>
    <button>Discard</button>
  </div>
  <main inert>
    <!-- cannot be keyboard focused or clicked -->
  </main>
</body>

A dialog is a great example, but inert is also helpful for things such as the slide-out side menu user experience. When a user slides out the side menu, it's not OK to let the mouse or keyboard interact with the page behind it; that's a bit tricky for users. Instead, when the side menu is showing, make the page inert, and now users must close or navigate within that side menu, and won't ever find themselves lost somewhere else in the page with an open menu.

Resources

COLRv1 Fonts

Before COLRv1 fonts, the web had OT-SVG fonts, also an open format for fonts with gradients and built-in colors and effects. These could grow very large though, and while they allowed editing the text, there wasn't much scope for customization.

After COLRv1 fonts, the web has smaller footprint, vector-scalable, repositionable, gradient-featuring, and blend-mode powered fonts that accept parameters to customize the font per use case or to match a brand.

Comparison visualization and bar chart, showing how COLRv1 fonts are sharper and smaller.
Image sourced from https://developer.chrome.com/blog/colrv1-fonts/

Here's an example from the Chrome Developer blog post about emojis. Maybe you've noticed that if you scale up the font size on an emoji, it doesn't stay sharp. It's an image and not vector art. Often in applications when an emoji is used, it's swapped out for a higher quality asset. With COLRv1 fonts, the emojis are vector and beautiful:

Icon fonts could do some amazing things with this format, offering custom duo-tone color palettes, and more. Loading a COLRv1 font is just like any other font file:

@import url(https://fonts.googleapis.com/css2?family=Bungee+Spice);

Customizing the COLRv1 font is done with @font-palette-values, a special CSS at-rule for grouping and naming a set of customization options into a bundle for later reference. Notice how you specify a custom name just like a custom property, starting with --:

@import url(https://fonts.googleapis.com/css2?family=Bungee+Spice);

@font-palette-values --colorized {
  font-family: "Bungee Spice";
  base-palette: 0;
  override-colors: 0 hotpink, 1 cyan, 2 white;
}

With --colorized as an alias for the customizations, the last step is to apply the palette to an element that is using the color font family:

@import url(https://fonts.googleapis.com/css2?family=Bungee+Spice);

@font-palette-values --colorized {
  font-family: "Bungee Spice";
  base-palette: 0;
  override-colors: 0 hotpink, 1 cyan, 2 white;
}

.spicy {
  font-family: "Bungee Spice";
  font-palette: --colorized;
}
Screenshot of the Bungee Spice font with the word DUNE.
Bungee Spice font shown with custom colors, source from https://developer.chrome.com/blog/colrv1-fonts/

With more and more variable fonts and color fonts becoming available, web typography is on a very magnificent path towards rich customization and creative expression.

Resources

Viewport units

Graphic showing how the device screen and the browser window and an iframe, all have different viewports.

Before the new viewport variants, the web offered physical units to assist in fitting viewports. There was one for height, width, smallest size (vmin), and largest side (vmax). These worked well for many things, but mobile browsers introduced a complexity.

On mobile, when loading a page, the status bar with the url is shown, and this bar consumes some of the viewport space. After a few seconds and some interactivity, the status bar may slide away to allow a bigger viewport experience for the user. But when that bar slides out, the viewport height has changed, and any vh units would shift and resize as their target size changed. In later years, the vh unit specifically needed to decide which of the two viewport sizes it was going to use, because it was causing jarring visual layout issues on mobile devices. It was determined that the vh would always represent the largest viewport.

.original-viewport-units {
  height: 100vh;
  width: 100vw;
  --size: 100vmin;
  --size: 100vmax;
}

After the new viewport variants, small, large, and dynamic viewport units are made available, with the addition of logical equivalents to the physical ones. The idea is to give developers and designers the ability to choose which unit they want to use for their given scenario. Maybe it's ok to have a small jarring layout shift when the status bar goes away, so then dvh (dynamic viewport height) could be used without worry.

A graphic with three phones to help illustrate DVH, LVH and SVH. The DVH
   example phone has two vertical lines, one between the bottom of the search bar
   and the bottom of the viewport and one between above the search bar (under the
   system status bar) to the bottom of the viewport; showing how DVH can be either
   of these two lengths. LVH is shown in the middle with one line between the
   bottom of the device status bar and the button of the phone viewport. The last is
   the SVH unit example, showing a line from the bottom of the browser search bar
   to the bottom of the viewport

Here's a complete list of all the new viewport unit options made available with the new viewport variants:

Height viewport units
​​.new-height-viewport-units {
  height: 100vh;
  height: 100dvh;
  height: 100svh;
  height: 100lvh;
  block-size: 100vb;
  block-size: 100dvb;
  block-size: 100svb;
  block-size: 100lvb;
}
Width viewport units
.new-width-viewport-units {
  width: 100vw;
  width: 100dvw;
  width: 100svw;
  width: 100lvw;
  inline-size: 100vi;
  inline-size: 100dvi;
  inline-size: 100svi;
  inline-size: 100lvi;
}
Smallest viewport side units
.new-min-viewport-units {
  --size: 100vmin;
  --size: 100dvmin;
  --size: 100svmin;
  --size: 100lvmin;
}
Largest viewport side units
.new-max-viewport-units {
  --size: 100vmax;
  --size: 100dvmax;
  --size: 100svmax;
  --size: 100lvmax;
}

Hopefully these will give developers and designers the flexibility needed to achieve their viewport responsive designs.

Resources

:has()

Browser Support

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Source

Before :has(), the subject of a selector was always at the end. For example, the subject of this selector is a list item: ul > li. Pseudo selectors can alter the selector but they don't change the subject: ul > li:hover or ul > li:not(.selected).

After :has(), a subject higher in the element tree can remain the subject while providing a query about children: ul:has(> li). It is easy to understand how :has() got a common name of "parent selector", as the subject of the selector is now the parent in this case.

Here's a basic syntax example where the class .parent remains the subject but is only selected if a child element has the .child class:

.parent:has(.child) {...}

Here's an example where a <section> element is the subject, but the selector only matches if one of the children has :focus-visible:

section:has(*:focus-visible) {...}

The :has() selector starts to become a fantastic utility once more practical use cases become apparent. For example, it's not currently possible to select <a> tags when they wrap images, making it difficult to teach the anchor tag how to change its styles when in that use case. It is possible with :has() though:

a:has(> img) {...}

These have all been examples where :has() only looks like a parent selector. Consider the use case of images inside of <figure> elements and adjusting styles on the images if the figure has a <figcaption>. In the following example, figures with figcaptions are selected and then images within that context. :has() is used and doesn't change the subject, as the subject we're targeting is images not figures:

figure:has(figcaption) img {...}

The combinations are seemingly endless. Combine :has() with quantity queries and adjust CSS grid layouts based on the number of children. Combine :has() with interactive pseudo class states and create applications that respond in new creative ways.

Checking for support is made simple with @supports and its selector() function, which tests if the browser understands the syntax before using it:

@supports (selector(:has(works))) {
  /* safe to use :has() */
}

Resources

2022 and beyond

There are still a number of things that will be hard to do after all these amazing features land in 2022. The next section takes a look at some of the remaining problems and the solutions that are actively being developed to resolve them. These solutions are experimental, even though they may be specified or available behind flags in browsers.

The upshot from the next sections should be comfort that the problems listed have many people from many companies seeking resolution—not that these solutions are going to be released in 2023.

Loosely typed custom properties

Browser Support

  • Chrome: 85.
  • Edge: 85.
  • Firefox: 128.
  • Safari: 16.4.

Source

CSS custom properties are amazing. They allow all sorts of things to be stored inside of a named variable, which then can be extended, calculated upon, shared, and more. In fact, they're so flexible, it would be nice to have some that are less flexible.

Consider a scenario where a box-shadow uses custom properties for its values:

box-shadow: var(--x) var(--y) var(--blur) var(--spread) var(--color);

This all works well until any one of the properties is changed into a value that CSS doesn't accept there, such as --x: red. The entire shadow breaks if any one of the nested variables is missing or is set to an invalid value type.

This is where @property comes in: --x can become a typed custom property, no longer loose and flexible, but safe with some defined boundaries:

@property --x {
  syntax: '<length>';
  initial-value: 0px;
  inherits: false;
}

Now, when the box-shadow uses var(--x) and later --x: red is attempted, red will be ignored as it's not a <length>. This means the shadow continues to work, even though an invalid value was given to one of its custom properties. Instead of failing, it reverts to its initial-value of 0px.

Animation

In addition to type safety, it also opens up many doors for animation. The flexibility of CSS syntax makes animating some things impossible, such as gradients. @property helps here because the typed CSS property can inform the browser about a developer's intent inside of otherwise overly complex interpolation. It essentially limits the scope of possibility insomuch that a browser can animate aspects of a style that it couldn't before.

Consider this demo example, where a radial gradient is used to make a portion of an overlay, creating a spotlight focus effect. JavaScript sets the mouse x and y when the alt/opt key is pressed, and then changes the focal-size to a smaller value such as 25%, creating the spotlight focus circle at the mouse position:

Try the demo
.focus-effect {
  --focal-size: 100%;
  --mouse-x: center;
  --mouse-y: center;

  mask-image: radial-gradient(
    circle at var(--mouse-x) var(--mouse-y),
    transparent 0%,
    transparent var(--focal-size),
    black 0%
  );
}

Gradients can't be animated though. They are too flexible and too complex for the browser to "just derive" how you want them to animate. With @property, though, one property can be typed and animated in isolation, for which the browser can easily understand the intent.

Video games that use this focus effect always animate the circle, from a large circle to a pinhole circle. Here's how to use @property with our demo so the browser animates the gradient mask:

@property --focal-size {
  syntax: '<length-percentage>';
  initial-value: 100%;
  inherits: false;
}

.focus-effect {
  --focal-size: 100%;
  --mouse-x: center;
  --mouse-y: center;

  mask-image: radial-gradient(
    circle at var(--mouse-x) var(--mouse-y),
    transparent 0%,
    transparent var(--focal-size),
    black 0%
  );

  transition: --focal-size .3s ease;
}
Try the demo

The browser is now able to animate the gradient size because we've reduced the surface area of the modification to just one property and typed the value so the browser can intelligently interpolate the lengths.

@property can do so much more, but these small enablements can go a long way.

Resources

Was in min-width or max-width

Before media query ranges, a CSS media query uses min-width and max-width to articulate over and under conditions. It may look like this:

@media (min-width: 320px) {
  
}

After media query ranges, the same media query could look like this:

@media (width >= 320px) {
  
}

A CSS media query using both min-width and max-width may look like this:

@media (min-width: 320px) and (max-width: 1280px) {
  
}

After media query ranges, the same media query could look like this:

@media (320px <= width <= 1280px) {
  
}

Depending on your coding background, one of those will look much more legible than the other. Thanks to the spec additions, developers will be able to choose which they prefer, or even use them interchangeably.

Resources

No media query variables

Before @custom-media, media queries had to repeat themselves over and over, or rely on preprocessors to generate the proper output based on static variables during build time.

After @custom-media, CSS allows aliasing media queries and the referencing of them, just like a custom property.

Naming things is very important: it can align purpose with the syntax, making things easier to share and easier to use in teams. Here are a few custom media queries that follow me between projects:

@custom-media --OSdark  (prefers-color-scheme: dark);
@custom-media --OSlight (prefers-color-scheme: light);

@custom-media --pointer (hover) and (pointer: coarse);
@custom-media --mouse   (hover) and (pointer: fine);

@custom-media --xxs-and-above (width >= 240px);
@custom-media --xxs-and-below (width <= 240px);

Now that they're defined, I can use one of them like this:

@media (--OSdark) {
  :root {
    
  }
}

Find a full list of custom media queries I use inside my CSS custom property library Open Props.

Resources

Nesting selectors is so nice

Before @nest, there was a lot of repetition in stylesheets. It became especially unwieldy when selectors were long and each was targeting small differences. The convenience of nesting is one of the most common reasons for adopting a preprocessor.

After @nest, the repetition is gone. Nearly every feature of preprocessor-enabled nesting will be made available built into CSS.

article {
  color: darkgray;
}

article > a {
  color: var(--link-color);
}

/* with @nest becomes */

article {
  color: darkgray;

  & > a {
    color: var(--link-color);
  }
}

What's most important about nesting to me, besides not repeating article in the nested selector, is the styling context remains within one style block. Instead of bouncing from one selector, and its styles, to another selector with styles (example 1), the reader can remain within the context of an article and see the article owns links inside of it. The relationship and style intent are bundled together, so article gets to appear to own its own styles.

The ownership could also be thought of as centralization. Instead of looking around a stylesheet for relevant styles, they can all be found nested together within a context. This works with parent to child relationships, but also with child to parent relationships.

Consider a component child that wants to adjust itself when in a different parent context, as opposed to the parent owning the style and changing a child:

/* parent owns this, adjusting children */
section:focus-within > article {
  border: 1px solid hotpink;
}

/* with @nest becomes */

/* article owns this, adjusting itself when inside a section:focus-within */
article {
  @nest section:focus-within > & {
     border: 1px solid hotpink;
  }
}

@nest helps overall with healthier style organization, centralization, and ownership. Components can group and own their own styles, instead of having them spread amongst other style blocks. It may seem small in these examples, but it can have very large impacts, for both convenience and legibility.

Resources

Scoping styles is really hard

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: behind a flag.
  • Safari: 17.4.

Source

Before @scope, many strategies existed because styles in CSS cascade, inherit, and are globally scoped by default. These features of CSS are very convenient for many things, but for complex sites and applications, with potentially many different styles of components, the global space and nature of the cascade can make styles feel like they're leaking.

After @scope, not only can styles be scoped to only within a context, like a class, they can also articulate where the styles end and do not continue to cascade or inherit.

In the following example, BEM naming convention scoping can be reversed into the actual intent. The BEM selector is attempting to scope the color of a header element to a .card container with naming conventions. This requires that the header has this classname on it, completing the goal. With @scope, no naming conventions are required in order to complete the same goal without marking up the header element:

.card__header {
  color: var(--text);
}

/* with @scope becomes */

@scope (.card) {
  header {
    color: var(--text);
  }
}

Here's another example, less component-specific and more about the global scope nature of CSS. Dark and light themes have to coexist inside a stylesheet, where order matters in determining a winning style. Usually this means dark theme styles come after the light theme; this establishes light as the default and dark as the optional style. Avoid the ordering and scope battling with @scope:

​​@scope (.light-theme) {
  a { color: purple; }
}

@scope (.dark-theme) {
  a { color: plum; }
}

To complete the story here, @scope also allows the establishing of where the style scope ends. This can't be done with any naming convention or preprocessor; it's special and only something CSS built-in to the browser can do. In the following example, img and .content styles are exclusively applied when a child of a .media-block is a sibling or parent of .content:

@scope (.media-block) to (.content) {
  img {
    border-radius: 50%;
  }

  .content {
    padding: 1em;
  }
}

Resources

No CSS way for a masonry layout

Before CSS masonry with grid, JavaScript was the best way to achieve a masonry layout, as any of the CSS methods with columns or flexbox would inaccurately represent the content order.

After CSS masonry with grid, no JavaScript libraries will be required and the content order will be correct.

Screenshot of the masonry layout which shows numbers traveling along the top, then going down.
Image and demo from Smashing Magazine
https://www.smashingmagazine.com/native-css-masonry-layout-css-grid/

The preceding demo is achieved with the following CSS:

.container {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: masonry;
}

It's comforting to know that this is on the radar as a missing layout strategy, plus you can try it today in Firefox.

Resources

CSS can't help users reduce data

Browser Support

  • Chrome: behind a flag.
  • Edge: behind a flag.
  • Firefox: not supported.
  • Safari: not supported.

Source

Before the prefers-reduced-data media query, JavaScript and a server could change their behavior based on a user’s operating system or browser "data saver" option, but CSS could not.

After the prefers-reduced-data media query, CSS can join the user experience enhancement and play its part in saving data.

@media (prefers-reduced-data: reduce) {
  picture, video {
    display: none;
  }
}

The preceding CSS is used in this media scroll component and the savings can be huge. Depending on how large the visiting viewport is, the more savings to be had on page load. Saving continues as users interact with the media scrollers. The images all have loading="lazy" attributes on them and that, combined with CSS hiding the element entirely, means a network request for the image is never made.

Screenshot of a TV show carousel interface with many thumbnails and titles shown.

For my testing, on a medium sized viewport, 40 requests and 700kb of resources were initially loaded. As a user scrolls the media selection, more requests and resources are loaded. With CSS and the reduced data media query, 10 requests and 172kb of resources are loaded. That's half a megabyte of savings and the user hasn't even scrolled any of the media, at which point there are no additional requests made.

Screenshot of a TV show carousel interface with no thumbnails and many titles shown.

There are more advantages to this reduced data experience than just data savings. More titles can be seen and there's no distracting cover images to steal attention. Many users browse in a data saver mode because they pay per megabyte of data—it's really nice to see CSS able to help out here.

Resources

Scroll snap features are too limited

Before these scroll snap proposals, writing your own JavaScript to manage a carousel, slider, or gallery could quickly get complex, with all the observers and state management. Also, if not careful, the natural scrolling speeds could get normalized by script, making user interaction feel a bit unnatural and potentially clunky.

New APIs

snapChanging()

As soon as the browser has released a snap child, this event fires. This allows UI to reflect the lack of a snap child and the indeterminate snap state of the scroller, as it's now being used and will land somewhere new.

document.querySelector('.snap-carousel').addEventListener('snapchanging', event => {
  console.log('Snap is changing', event.snappedTargetsList);
});
snapChanged()

As soon as the browser has snapped to a new child and the scroller is rested, this event fires. This lets any UI that depends on the snapped child to update and reflect the connection.

document.querySelector('.snap-carousel').addEventListener('snapchanged', event => {
  console.log('Snap changed', event.snappedTargetsList);
});
scroll-start

Scrolling doesn't always begin at the start. Consider swipeable components where swiping left or right triggers different events, or a search bar that on page load is initially hidden until you scroll to the top. This CSS property lets developers specify that a scroller should begin at a specific point.

:root { --nav-height: 100px }

.snap-scroll-y {
  scroll-start-y: var(--nav-height);
}
:snap-target

This CSS selector will match elements in a scroll snap container that are currently snapped by the browser.

.card {
  --shadow-distance: 5px;
  box-shadow: 0 var(--shadow-distance) 5px hsl(0 0% 0% / 25%);
  transition: box-shadow 350ms ease;
}

.card:snapped {
  --shadow-distance: 30px;
}

After these scroll snap proposals, making a slider, carousel, or gallery is much easier as the browser now offers conveniences for the task, eliminating observers and scroll orchestration code in favor of using built-in APIs.

It's still very early days for these CSS and JS features, but be on the lookout for polyfills that can help adoption, and testing, of them soon.

Resources

Cycling between known states

Before toggle(), only states built into the browser already could be leveraged for styling and interaction. The checkbox input, for example, has :checked, an internally managed browser state for the input that CSS is able to use for changing the element visually.

After toggle(), custom states can be created on any element for CSS to change and use for styling. It allows groups, cycling, directed toggling, and more.

In the following example, the same effect of a list item strikethrough on complete is achieved but without any checkbox elements:

<ul class='ingredients'>
   <li>1 banana
   <li>1 cup blueberries
  ...
</ul>

And the relevant CSS toggle() styles:

li {
  toggle-root: check self;
}

li:toggle(check) {
  text-decoration: line-through;
}

If you're familiar with state machines, you may notice how much crossover there is with toggle(). This feature will let developers build more of their state into CSS, hopefully resulting in clearer and more semantic ways of orchestrating interaction and state.

Resources

Customizing select elements

Before <selectmenu>, CSS didn't have the ability to customize <option> elements with rich HTML or change much about the display of a list of options. This led developers to load external libraries that recreated much of the functionality of a <select>, which ended up being a lot of work.

After <selectmenu>, developers can provide rich HTML for options elements and style them as much as they need, while still meeting accessibility requirements and providing semantic HTML.

In the following example, taken from the <selectmenu> explainer page, a new select menu is created with some basic options:

<selectmenu>
  <option>Option 1</option>
  <option>Option 2</option>
  <option>Option 3</option>
</selectmenu>

CSS can target and style the element's parts:

.my-select-menu::part(button) {
  color: white;
  background-color: red;
  padding: 5px;
  border-radius: 5px;
}

.my-select-menu::part(listbox) {
  padding: 10px;
  margin-top: 5px;
  border: 1px solid red;
  border-radius: 5px;
}

A select looking menu with red accent colors.

You can try the <selectmenu> element on Chromium in Canary with the web experiments flag enabled. Watch out in 2023 and beyond for customizable select menu elements.

Resources

Anchoring an element to another

Before anchor(), position absolute and relative were position strategies provided for developers to have child elements move around within a parent element.

After anchor(), developers can position elements to other elements, regardless of them being a child or not. It also allows developers to specify which edge to position against, and other niceties for creating position relationships between elements.

The explainer has a few great examples and code samples provided, if you're interested in learning more.

Resources