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 Support | Full |
Progressive Enhancement? | Potentially — if it’s not critical what you are styling, then yes. |
Polyfillable | Yes |
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. |
Polyfillable | No |
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 Support | Full |
Progressive Enhancement? | Yes — you could list a declaration using fallback units right before the declaration using container query units. |
Polyfillable | Yes |
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
Basic Demo of Usage
View Transitions
What are View Transitions?
There are two types of View Transitions:
- Same-Page Transitions (Require JavaScript)
- 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. |
Polyfillable | No |
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 Support | Full |
Progressive Enhancement? | No |
Polyfillable | You 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. |
Polyfillable | Yes |
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. |
Polyfillable | Yes |
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 |
Polyfillable | No |
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
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
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 Support | Full (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. |
Polyfillable | Yes |
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 Support | Full |
Progressive Enhancement? | Yes, you could declare fallback colors. |
Polyfillable | Could 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: CSS (css)
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. |
Polyfillable | No |
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
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 Support | Depends 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. |
Polyfillable | Yes. |
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 Support | Full |
Progressive Enhancement? | Yes. You can fall back to defining your own grid lines that are workable if not perfect. |
Polyfillable | No. 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 & Functions — actual 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.