What You Need to Know about Modern CSS (Spring 2024 Edition)

My goal with this bookmarkable guide is to provide a list of (frankly: incredible) new additions to CSS lately. There is no hardline criteria for this list other than that these things are all fairly new and my sense is that many people aren’t aware of these things. Or even if they are, they don’t have a great understanding of them and could use a plain language explanation of what it is, why they should care, and a bit of reference code. Maybe that’s you.

I’d like to work on our collective muscle memory on these features. Like I said, “even if you know about this stuff, it takes time to build the muscle memory around it.”

There is quite a bit more syntax, detail, and nuance to these things than I am presenting here. I want you to know what’s possible, reference the most basic usage and syntax, then dig deeper when you need to.

Container Queries (Size)

What are Size Container Queries?

Container Queries allow you to write styles that apply to the children of a container element when that container matches certain media conditions, typically a width measurement.

<div class="element-wrap">
  <div class="element">
  </div>
</div>Code language: HTML, XML (xml)
.element-wrap {
  container: element / inline-size;
}
@container element (min-inline-size: 300px) {
  .element {
    display: flex;
    gap: 1rem;
  }
}Code language: CSS (css)

When should you care?

If you’ve ever thought: I wish I could make styling decisions based on the size of this element, not the entire page like @media queries force me to do. Then using @container queries are for you! People who work on design systems or heavily component-based websites will probably mostly use Container Queries to style things based on size, because the size of the entire page is a poor proxy for the size of a component.

Support

Browser SupportFull
Progressive Enhancement?Potentially — if it’s not critical what you are styling, then yes.
PolyfillableYes

Basic Demo of Usage

Use the resizer in the middle to see the calendar change layout depending on how much space it has. It has three breakpoints of its own.

Container Queries (Style)

What are Style Container Queries?

Container Style Queries allow you to apply styles when a given Custom Property has a given value.

.container {
  --variant: 1;

  &.variant2 {
    --variant: 2;
  }
}

@container style(--variant: 1) {
  button { } /* You can't style .container, but can select inside it */
  .other-things { }
}

@container style(--variant: 2) {
  button { }
  .whatever { }
}Code language: CSS (css)

When should you care?

Have you ever wanted a mixin in CSS? As in, you set one property but get multiple properties. Sass made mixins fairly popular. You can do that with a Style Container Query. But just like how Sass had variables then CSS variables turned out to be more powerful and useful, Style Container Queries are likely to be more powerful and useful, because they respect the cascade and can be calculated and such.

Support

Browser Support✅ Chrome ‘n’ Friends
🔜 Safari
❌ Firefox
Progressive Enhancement?Potentially — It depends on what you’re doing with the styles, but let’s say not really.
PolyfillableNo

Basic Demo of Usage

Container Units

What are Container Units?

Container Units (literally units, like px, rem, or vw) allow you to set the size of things based on the current size of a container element. Similar to how with viewport units 1vw is 1% of the browser window width, 1cqw is 1% of the width of the container (although I’d recommend you use cqi instead, the “logical equivalent”, meaning the “inline direction”).

The units are cqw (“container query width”), cqh (“container query height”), cqi (“container query inline”), cqb (“container query block”), cqmin (smaller of cqi and cqb), and cqmax (larger of cqi and cqb).

When should you care?

If the sizing of anything in an element feels as if it should be based on the current size of the container, container units are essentially the only way. An example of this is typography. A typical Card element may deserve a larger header text when it happens to be rendered larger, without something like a class name needing to be added to control that. (I’m a fan of this demo.) Even a container query is clunky comparatively.

Support

Browser SupportFull
Progressive Enhancement?Yes — you could list a declaration using fallback units right before the declaration using container query units.
PolyfillableYes

Basic Demo of Usage

This element uses container query units for not only the font-size, but the padding, border, and margin as well.

The :has() Pseudo Selector

What is the :has() selector?

The :has() selector allows you to conditionally select an element when elements deeper in the DOM tree of the original element match the selector you put inside :has().

figure:has(figcaption) {
  border: 1px solid black;
  padding: 0.5rem;
}Code language: CSS (css)

When should you care?

If you’ve ever wanted a “parent” selector in CSS, :has() can do that, but it’s more powerful than that, as once you’ve selected the parent you want, you can again drill back down. Jhey Tompkins once called it a “family selector” which a nice way to think about it. You can also combine it with :not() to build a selector when an element doesn’t “have” a matching element inside.

Support

Browser SupportFull
Progressive Enhancement?Depends on what you’re doing with the styles, but let’s say not really.
PolyfillableFor the JavaScript side only

Basic Demo of Usage

View Transitions

What are View Transitions?

There are two types of View Transitions:

  1. Same-Page Transitions (Require JavaScript)
  2. Multi-Page Transitions (CSS Only)

They are both useful. A same-page transition involves and animation when the DOM is changed without the page changing, like a list being sorted. A multi-page transition is for animating elements between page loads, like a video thumbnail transitioning into a video element. This is the basic syntax for a same-page transition:

if (!document.startViewTransition) {
  updateTheDOM();
} else {
  document.startViewTransition(() => updateTheDOM());
}Code language: JavaScript (javascript)

For multi-page transitions: you need this meta tag:

<meta name="view-transition" content="same-origin">Code language: HTML, XML (xml)

Then any element you want to transition between pages you make sure has a totally unique view-transition-name applied in the styles, on both the outgoing page and incoming page.

When should you care?

Users can understand an interface better if an element moves to a new position rather than instantly being there. There is an animation concept called tweening where the animation is automatically created based on a starting and ending state. View Transitions are essentially tweening. You can control aspects of the animation, but for the most part the animation is automatically created based on the starting and ending state of the DOM, rather than you having to be really specific about the animation details.

Support

Browser Support✅ Chrome ‘n’ Friends
❌ Safari
❌ Firefox
Progressive Enhancement?Yes — the transitions can just not run, or you could provide a fallback animation.
PolyfillableNo

Basic Demo of Usage

This is an example of a same-page view transition:

Nesting

What is nesting?

Nesting is a way of writing CSS that allow you to write additional selectors within an existing ruleset.

.card {
  padding: 1rem;

  > h2:first-child {
    margin-block-start: 0;
  }

  footer {
    border-block-start: 1px solid black;
  }
}
Code language: CSS (css)

When should you care?

Nesting is mostly a CSS authoring convenience, but the fact that it can group related CSS nicely together and prevent you from having to repeat writing a selector can mean avoiding mistakes and making the CSS easier to read. Nested CSS can also be something of a footgun in that may encourage writing CSS that matches the nesting of HTML in an unnecessary way, increasing the specificity and decreasing the reusability of some CSS.

.card {
  container: card / inline-size;
  
  display: grid;
  gap: 1rem;
  
  @container (min-inline-size: 250px) {
    gap: 2rem;
  }
}
Code language: CSS (css)

The only major difference from Sass-style nesting is that you can’t combine the & directly.

.card {
  body.home & { /* totally fine */ }
  & .footer { /* totally fine, don't even need the & */
  &__big { /* nope, can't do this */ }
}Code language: CSS (css)

Support

Browser SupportFull
Progressive Enhancement?No
PolyfillableYou could use a processor like LightningCSS, Sass, Less, etc.

Scroll-Driven Animations

What are Scroll-Driven Animations?

Any animation that is tied to the scrolling of an element (often the page itself) can now be done in CSS rather than needing to bind DOM scrolling events in JavaScript. They come in two varieties:

  • The scroll progress of the element. (animation-timeline: scroll())
  • An element’s current viewable position within the element. (animation-timeline: view())

When should you care?

Imagine a reading progress indicator bar that fills from 0% to 100% as the user scrolls down the page. That can be done with an animation moving the background-position of an element tried to the overall scroll position of the page. Doing this in CSS instead of JavaScript is good for performance.

The other major use case covered by scroll-driven animations is to run an animation as an element enters (or leaves!) the viewport. You have lots of control over the details, like when the animation starts and ends based on how visible the element is.

Support

Browser Support✅ Chrome ‘n’ Friends
❌ Safari
🔜 Firefox
Progressive Enhancement?Yes — these effects tend to be visual flair, not required functionality.
PolyfillableYes

Basic Example of Usage

This is the demo from when we looked at image zooming and page scrolling.

Anchor Positioning

What is Anchor Positioning?

Anchor positioning allows you to place items relative to where another element is. Seems pretty obvious when put like that, but that’s what it is. You declare an element an anchor and give it a name, then can position elements to the top/right/bottom/left (or center, or the logical equivalents) of the anchor.

When should you care?

Once you can use this freely, you’ll have to care less about exact DOM positioning of elements (aside from accessibility concerns). The way it is now, the element you want to position relative to another has to be a child element and for there to be a positioning context to work within. This can dictate where elements go in the DOM, whether or not that makes sense.

The big use cases are going to be tooltips and custom context menus.

Support

Browser Support🔜 Chrome ‘n’ Friends
❌ Safari
❌ Firefox
Progressive Enhancement?Possibly — if you can tolerate a totally different position for elements.
PolyfillableYes

Basic Example of Usage

At the time I’m publishing this, this only works in Chrome Canary with the “Experimental Web Platform Features” flag enabled.

Scoping

What is Scoped CSS?

Scoping in CSS is in the form of an @scope at-rule that declares a block of CSS to only apply to the given selector. And optionally, stop applying at another given selector.

When should you care?

You can also scope CSS by applying a class and nesting within that class. But @scope has a few tricks up it’s sleeve that can make it interesting. The “donut scope” option is a unique ability it has:

@scope (.card) to (.markdown-output) {
  h2 {
    background: tan; /* stop doing this when we get to the Markdown */
  }
}Code language: CSS (css)

More logical proximity styling is another useful feature. This is a bit tricky to explain but once you see it you can’t unsee it. Consider theming. You have a .dark selector and a .light selector. If you only ever use one on the entire page, that’s fine, but if you end up nesting them at all, because they have the same specificity, whichever one you define later is technically a bit more powerful, and can win out even if it doesn’t make sense to. Minimal example:

.purple-paragraphs p { color: purple; }
.red-paragraphs p { color: red; }Code language: CSS (css)
<div class="purple-paragraphs">
  <div class="red-paragraphs">
    <div class="purple-paragraphs">
       <p>some text</p>
    </div>
  </div>
</div>Code language: HTML, XML (xml)

You might think the paragraph element in there would have the color purple, but it will actually be red. That’s just awkward, but it can be fixed with @scope. When scoped selectors match, as Bramus says, “it weighs both selectors by proximity to their scoping root”, and since “light” is closer here, it would win.

My favorite though is the ability to drop in a <style> tag in the DOM and have it apply scoped styles only to that bit of the DOM, without having to name anything.

<div class="my-cool-component">
  <style>
    @scope {
      :scope { /* selects the div above, without having to select it by class or anything */
      }
      .card {
      }
    }
  </style>

  <article class="card">
  </article>
</div>Code language: HTML, XML (xml)

Support

Browser Support✅ Chrome
✅ Safari
❌ Firefox
Progressive Enhancement?No
PolyfillableNo

Basic Example of Usage

Cascade Layers

What are Layers?

Cascade Layers in CSS are an extremely powerful syntax that affects the styling strength of a chunk of styles. You can optionally name and order layers (if you don’t explicitly order them, they order in source order). Styles in higher layers automatically beat styles in lower layers, regardless of selector strength. Styles not within layers are the most powerful.

<body id="home">Code language: HTML, XML (xml)
@layer base {
  body#home {
    margin: 0;
    background: #eee;
  }
}

body {
  background: white;
}
Code language: CSS (css)

We’re used to thinking that body#home is a much more powerful selector, thus the background will be #eee. But because there are unlayered styles here, that will win, making the background white.

You may have as many layers as you like and can order them upfront. I think layering is likely to become a best practice on new greenfield projects, and take shape something like:

@layer reset, default, themes, patterns, layouts, components, utilities;Code language: CSS (css)

One gotcha is that !important rules on lower layers are actually more powerful.

When should you care?

One clear way you get a lot of value out of CSS layers if you work on a project that uses a third-party styling library. You can put that library on a lower layer than the styles that your team writes, and you won’t have to worry about fighting the third-party library in terms of selector strength. Your styles on a higher layer will always win, which is likely to create cleaner and more maintainable CSS.

For example, put all of Bootstrap on a lower layer just using the layer keyword and then any styles you write after that will win, even if Bootstrap itself uses a higher power selector.

@import url("https://cdn.com/bootstrap.css") layer;

h5 {
  margin-bottom: 2rem;
}Code language: CSS (css)

Support

Browser SupportFull
Progressive Enhancement?No
PolyfillableYes

Basic Example of Usage

Logical Properties

What are Logical Properties?

Logical properties are alternatives to properties that specify a direction. For example, in a left-to-right language like English, the inline direction is horizontal and the block direction is vertical, so margin-right is equivalent to margin-inline-end and margin-top is equivelant to margin-block-start. In a right-to-left language like Arabic, margin-inline-end changes to the equivalent of margin-left, because that is the end side of the inline flow of elements. There are a lot of CSS properties and values that have a directional component like this (border, padding, offset, set), so the trick is understanding inline and block flow and using the correct start or end value.

When should you care?

Often when you are declaring directional information in CSS, what you mean is “in the inline direction of text”. That might sound strange, but imagine a button and the space between an icon and the text. If you apply margin-right to the icon to space it away from the text, but then the page is translated to a right-to-left language, that spacing is now on the wrong side of the icon. What you meant was margin-inline-end on the icon. If you code your side using logical properties in this way, it will automatically translate better without writing any additional conditional code.

Support

Browser SupportFull
Progressive Enhancement?You’d have to use @supports and unset to remove the fallback value and reset using a logical property, but it’s possible.
PolyfillableI can’t vouch for it, but there is a processing option.

Basic Example of Usage

P3 Colors

What is the Display P3 Color Space?

We’re largely used to the sRGB color space on the web. That’s what hex colors use, and the rgb(), hsl(), and hsb() functions. Many displays these days are capable of display a much wider range of colors than sRGB is capable of describing, so being limited to that color space sucks. The Display P3 color space is about 50% wider than sRGB, expanding in the direction of more rich and vibrant colors. New CSS functions, which can even use different color models that have their own useful properties, allow us to declare colors in this space.

When should you care?

If you want to use colors that are quite vibrant, you’ll need to tap into colors in the P3 Color Space. Using newer color models (and functions) can do this, and are very useful for a variety of other things.

For example, the oklch() function (and thus OKLCH color model) can display any color any other method can (plus P3), has a similar human readability in common with hsl(), and has “uniform perceived brightness”, so that the first number (lightness) behaves way more predictably than it does in hsl(). That’s awfully nice for color on the web. But it’s not the only new color model and function! I find the oklab color model generally best for gradients.

Support

Browser SupportFull (e.g. oklab)
Progressive Enhancement?Yes — you can declare fallback colors and displays that can’t display the color you declare will come back down into range.
PolyfillableYes

Basic Example of Usage

You can edit these <style> blocks because I made them display: block; and contenteditable:

Color Mixing

What is color-mix()?

The color-mix() function in CSS allows you to, wait for it, mix colors. This kind of thing has been baked into CSS processing tools for a long time, and as typical of CSS evolution, now that it’s in native CSS, it’s more thoughtful and powerful than it ever was in a processor.

When you should care?

Have you ever wanted to darken or lighten a color you already have on the fly? That’s one of the things color-mix() can do. Color mixing can happen in a specific color model which means you can take advantage of how that models works. For example, the perceptually uniform brightness of OKLCH makes it sensible to use for adjusting brightness. You can make whole color palettes using color-mix().

Browser Support

Browser SupportFull
Progressive Enhancement?Yes, you could declare fallback colors.
PolyfillableCould be but I don’t know of one.

Basic Example of Usage

Margin Trim

What is margin-trim?

The margin-trim property removes any margin in the direction specified from the selected container at the end of that direction. Imagine you have five blocks in a row that all have right margin on them in a container. You might select the :last-child to remove the right margin. With margin-trim you can ensure that margin is removed from the parent element itself.

.container {
  /* prevent "extra" margin at the end of the element */
  margin-trim: block-end;

  /* an element like this might be the culprit, but it could be anything */
  > p {
    margin-block-end: 1rem;
  }
}Code language: JavaScript (javascript)

When should you care?

You know how the gap property of flexbox and grid is… awesome? It only puts spacing between elements. Well, if you need to apply spacing between elements but you’re in a position where you can’t use gap, margin-trim is awfully nice as it means you apply directional margin to all the children and not worry about an additional fancy selector to select the first or last one and remove that unneeded final margin. It might end up a best practice.

Support

Browser Support✅ Safari
❌ Chrome
❌ Firefox
Progressive Enhancement?Yes. A little extra space likely isn’t a major problem.
PolyfillableNo

Basic Example of Usage

The last paragraph here is a notorious situation where the bottom margin on it creates more space at the bottom than any of the other edges. With margin-trim we can ensure it’s sliced off without having to select that last paragraph and manually remove it.

Text Wrapping

What is text-wrap?

The text-wrap property likely isn’t in your long term CSS memory. It’s capable of text-wrap: nowrap;, but we generally think of white-space: nowrap; for this. But now, text-wrap has two new tricks up it’s sleeve:

  • text-wrap: balance; — Attempt to make equal-width lines when text wraps.
  • text-wrap: pretty; — Avoid orphans.

When should you care?

A headline with one word orphaned onto the next line just looks really awkward and could be considered poor typography. There wasn’t a great way to solve this before, short of somewhat awkward tricks like inserting a &nbsp; instead of a normal space between the last two words. Balancing a headline prevents this, but goes further in making the multiple lines of text generally the same width. Using pretty is more focused just on orphan prevention alone, making it more appropriate for body text.

Support

Browser SupportDepends on which value. balance has decent support with all browsers ready or coming soon. pretty, less-so.
Progressive Enhancement?Yes. While slightly less aesthetic, widows and orphans are not that big of a problem, so if this property doesn’t work, it’s no big deal.
PolyfillableYes.

Basic Example of Usage

Subgrid

What is Subgrid?

Subgrid is an optional part of using CSS grid that is relevant when you are nesting gridded elements. By setting grid-template-columns: subgrid; or grid-template-rows: subgrid; on a grid-level element, you’re saying “inherit these columns or rows from my parent grid, where relevant”.

When should you care?

The point of using grids for layout is generally lining things up. Without subgrid, it means that child elements of a grid don’t have access to the grid lines of the parent grid, and thus lack the opportunity help line things up. Subgrid fills that gap. When DOM nesting is important for functionality or accessibility, like in a <form>, subgrid can help ensure things line up sensibly.

Support

Browser SupportFull
Progressive Enhancement?Yes. You can fall back to defining your own grid lines that are workable if not perfect.
PolyfillableNo. There is a grid polyfill but it doesn’t do subgrid.

Basic Example of Usage

Things to keep an eye on…

The speed of CSS development doesn’t seem to have slowed down. There is plenty to continue to watch for and look forward to.

  • CSS Mixins & Functionsactual mixins and functions that take parameters
  • Relative Color Syntax — a way to manipulate the parts of colors in an intuitive and powerful way.
  • Interop 2024 — All the things that we can essentially bet on for being cross-browser compatible soon, including the relative color syntax above.
  • The CSS property field-sizing should help the long-standing difficult issue of auto-resizing form elements like textareas and input to the content they contain.
  • <selectmenu> in HTML is essentially a fully CSS styleable <select>, which is wild.

That’s just a few things to watch. You might as well subscribe to our feed as we’ll be doing the watching for you and then you’ll catch the next one.

Did I miss a relatively new favorite of yours? Let me know.

6 responses to “What You Need to Know about Modern CSS (Spring 2024 Edition)”

  1. Avatar Oliver says:

    Very helpful summary!

  2. Avatar Mauricio Poveda says:

    Awesome post by one the most CSS saavy devs in CSS. Very helpful and thankful to FEM to post this contents free.

  3. Avatar s427 says:

    Very helpful article, thank you very much!

    However, please note that in “Scoping”, the example given with the three embedded divs and light/dark classes does not work. Contrary to what the article says, “some text” will use the “light” style. I am guessing this is an overly simplified version of the example given on the MDN page for @scope, which is slightly more convoluted: https://developer.mozilla.org/en-US/docs/Web/CSS/@scope#how_scope_conflicts_are_resolved

  4. Avatar Michael Berger says:

    This is very useful article. However, I have a big problem with container queries, at least as you have described them. Problem 1: You are writing if/then code in CSS: that should be in JavaScript.
    Problem 2: The bigger issue: you are writing more code then you need to. In the example you used, you could have been handled the situation much easier, and with less code, with a class. style=”–variant: 1;” is class1 and style=”–variant: 2;” is class2. Your code would be simpler and easier to read in HTML, CSS and JS.

  5. Avatar Chris Coyier says:

    You are writing if/then code in CSS: that should be in JavaScript.

    Do you feel that way about media queries? Hover states? Focus styles? Disabled styles? Validity checks? Heck, every selector is an if/then statement. If the selector matches, do the styles.

    you could have been handled the situation much easier, and with less code, with a class. style=”–variant: 1;” is class1 and style=”–variant: 2;” is class2. Your code would be simpler and easier to read

    I agree class changes are a very clear way to change the state of something. But changing the value of a custom property in CSS is a different beast entirely. You could change a custom property because of a :has() selector, or a media query, or a state change, or a million other things. In those cases, you don’t have the ability to change a class in the HTML. But with a style query, you can still change a whole chunk of styles when the custom property changes.

Leave a Reply

Your email address will not be published. Required fields are marked *