Writing CSS In 2023: Is It Any Different Than A Few Years Ago?

About The Author

Geoff is a technical editor and writer at Smashing Magazine. On top of that, he’s an independent contractor who teaches front-end design and development classes … More about Geoff ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

CSS is evolving faster than ever. With all of the new features that are now available — and forthcoming — since we got Flexbox and Grid years ago, the way we write CSS is evolving, too. In this article, Geoff Graham shares which features have had the most influence on his current approaches to CSS, as well as those that have not (at least yet).

Is there anything in the front-end world that’s evolving faster than CSS these days? After what seemed like a long lull following blockbusters Flexbox and Grid, watching CSS release new features over the past few years has been more like watching a wild game of rugby on the telly. The pace is exciting, if not overwhelming at the same time.

But have all these bells and whistles actually changed the way you write CSS? New features have certainly influenced the way I write CSS today, but perhaps not quite as radically as I would have expected.

And while I’ve seen no shortage of blog posts with high-level examples and creative experiments of all these newfangled things that are available to us, I have yet to see practical applications make their way into production or everyday use. I remember when Sass started finding its way into CSS tutorials, often used as the go-to syntax for code examples and snippets. I’m not exactly seeing that same organic adoption happen with, say, logical properties, and we’ve had full browser support for them for about two years now.

This isn’t to rag on anyone or anything. I, for one, am stoked beyond all heck about how CSS is evolving. Many of the latest features are ones we have craved for many, many years. And indeed, there are several of them finding their way into my CSS. Again, not drastically, but enough that I’m enjoying writing CSS more now than ever.

Let me count the ways.

More And More Container Queries

I’ll say it: I’ve never loved writing media queries for responsive layouts. Content responds differently to the width of the viewport depending on the component it’s in. And balancing the content in one component has always been a juggling act with balancing the content in other components, adding up to a mess of media queries at seemingly arbitrary breakpoints. Nesting media queries inside a selector with Sass has made it tolerable, but not to the extent that I “enjoyed” writing new queries and modifying existing ones each time a new design with UI changes is handed to me.

Container queries are the right answer for me. Now I can scope child elements to a parent container and rely on the container’s size for defining where the layout shifts without paying any mind to other surrounding components.

The other thing I like about container queries is that they feel very CSS-y. Defining a container directly on a selector matches a natural property-value syntax and helps me avoid having to figure out math upfront to determine breakpoints.

.parent {
  container-type: inline-size;
}

@container (min-width: 600px) {
  .child {
    align-self: center;
  }
}

I still use media queries for responsive layouts but tend to reserve them for “bigger” layouts that are made up of assembled containers. Breakpoints are more predictable (and can actually more explicitly target specific devices) when there’s no need to consider what is happening inside each individual container.

Learn About Container Queries

Grouping Styles In Layers

I love this way of managing the cascade! Now, if I have a reset or some third-party CSS from a framework or whatever, I can wrap those in a cascade layer and chuck them at the bottom of a file so my own styles are front and center.

I have yet to ship anything using cascade layers, but I now reach for them for nearly every CodePen demo I make. The browser support is there, so that’s no issue. It’s more that I still rely on Sass on projects for certain affordances, and maintaining styles in partialized files still feels nice to me, at least for that sort of work.

But in an isolated demo where all my styles are in one place, like CodePen? Yeah, all the cascade layers, please! Well, all I really need is one layer for the base styles since un-layered styles have higher specificity than layered ones. That leaves my demo-specific styles clean, uncluttered, and still able to override the base at the top, which makes it way more convenient to access them.

body {
  display: grid;
  gap: 3rem;
  place-items: center;
}

@layer base {
  body {
    font-size: 1.25rem;
    line-height: 1.35;
    padding: 3rem;
  }
}

Learn More About Cascade Layers

:is() And :where()

I definitely reach for these newer relational pseudo-selectors, but not really for the benefits of selecting elements conditionally based on relationships.

Instead, I use them most often for managing specificity. But unlike cascade layers, I actually use these in production.

Why? Because with :is(), specificity is determined not by the main selector but by the most specific selector in its argument list.

/* Specificity: 0 1 1 */
:is(ol, .list, ul) li {}

/* Specificity: 0 0 2 */
ol li {}

The .list selector gives the first ruleset a higher specificity score meaning it “beats” the second ruleset even though the first ruleset is higher in the cascade.

On the flip side, the specificity of :where() is a big ol’ score of zero, so it does not add to the overall score of whatever selector it’s on. It simply doesn’t matter at all what’s in its argument list. For the same reason I use :is() to add specificity, I use :where() to strip it out. I love keeping specificity generally low because I still want the cascade to operate with as little friction as possible, and :where() makes that possible, especially for defining global styles.

A perfect example is wrapping :not() inside :where() to prevent :not() from bumping up specificity:

/* Specificity: 0 0 0 */
:where(:not(.some-element)) {}

Taken together, `:is()` and `:where()` not only help manage specificity but also take some cognitive load from “naming” things.

I’m one of those folks who still love the BEM syntax. But naming is one of the hardest things about it. I often find myself running out of names that help describe the function of an element and its relationship to elements around it. The specificity-wrangling powers of :is() and :where() means I can rely less on elaborate class names and more on element selectors instead.

Learn More About :is() And :where()

The New Color Function Syntax

The updated syntax for color functions like rgb() and hsl() (and the evolving oklch() and oklab()) isn’t the sort of attention-grabbing headline that leads to oo’s and aw’s, but it sure does make it a lot better to define color values.

For one, I never have to reach for rgba() or hsla() when I need an alpha value. In fact, I always used those whether or not I needed alpha because I didn’t want to bother deciding which version to use.

color: hsl(50deg, 100%, 50%);

/* Same */
color: hsla(50deg, 100%, 50% / 1)

Yes, writing the extra a, /, and 1 was worth the cost of not having to think about which function to use.

But the updated color syntax is like a honey badger: it just doesn’t care. It doesn’t care about the extra a in the function name. It doesn’t even care about commas.

color: hsl(50deg 100% 50% / .5);

So, yeah. That’s definitely changed the way I write colors in CSS.

What I’m really excited to start using is the newer oklch() and oklab() color spaces now that they have full browser support!

Learn More About CSS Color 4 Features

Sniffing Out User Preferences

I think a lot of us were pretty stoked when we got media queries that respect a user’s display preferences, the key one being the user’s preferred color scheme for quickly creating dark and light interfaces.

:root {
  --bg-color: hsl(0deg 0% 100%);
  --text-color: hsl(0deg 0% 0%);
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-color: hsl(0deg 0% 0%);
    --text-color: hsl(0deg 0% 100%);
  }
}

body {
  background: var(--bg-color);
  color: var(--text-color);
}

But it’s the prefers-reduced-motion query that has changed my CSS the most. It’s the first thing I think about any time a project involves CSS animations and transitions. I love the idea that a reduced motion preference doesn’t mean nuking all animation, so I’ll often use prefers-reduced-motion to slow everything down when that’s the preference. That means I have something like this (usually in a cascade layer for base styles):

@layer base {
  :root {
    --anim-duration: 1s;
  }

  /* Reduced motion by default */
  body {
    animation-duration: --anim-duration;
    transition: --anim-duration;
  }

  /* Opt into increased motion */
  @media screen and (prefers-reduced-motion: no-preference) {
    body {
      --anim-duration: .25s;
    }
  }
}

Learn More About User Preference Queries

Defining Color Palettes

I’ve used variables for defining and assigning colors ever since I adopted Sass and was thrilled when CSS custom properties came. I’d give generic names to the colors in a palette before passing them into variables with more functional names.

/* Color Palette */
--red: #ff0000;
/* etc. */

/* Brand Colors */
--color-primary: var(--red);
/* etc. */

I still do this, but now I will abstract things even further using color functions on projects with big palettes:

:root {
  /* Primary Color HSL */
  --h: 21deg;
  --s: 100%;
  --l: 50%;
  
  --color-primary: hsl(var(--h) var(--s) var(--l) / 1);
}

.bg-color {
  background: var(--color-primary);
}

.bg-color--secondary {
  --h: 56deg;
  background: hsl(var(--h) var(--s) var(--l) / 1);
}

A little too abstract? Maybe. But for those projects where you might have ten different varieties of red, orange, yellow, and so on, it’s nice to have this level of fine-grained control to manipulate them. Perhaps there is more control with color-mix() that I just haven’t explored yet.

Learn More About Defining Color Palettes

What I’m Not Using

Huh, I guess I am writing CSS a bit differently than I used to! It just doesn’t feel like it, but that probably has to do with the fact that there are so many other new features I am not currently using. The number of new features I am using is much, much lower than the number of features I have yet to pick up, whether it’s because of browser support or because I just haven’t gotten to it yet.

CSS Nesting

I’m really looking forward to this because it just might be the tipping point where I completely drop Sass for vanilla CSS. It’s waiting for Firefox to support it at the time of this writing, so it could be right around the corner.

Style Queries

I’ve made no secret that applying styles to elements based on the styles of other elements is something that I’m really interested in. That might be more of an academic interest because specific use cases for style queries elude me. Maybe that will change as they gain browser support, and we see a lot more blog posts where smart folks experiment with them.

:has()

I’ll definitely use this when Firefox supports it. Until then, I’ve merely tinkered with it and have enjoyed how others have been experimenting with it. Without full support, though, it hasn’t changed the way I write CSS. I expect that it will, though, because how can having the ability to select a parent element based on the child it contains be a bad thing, right?

Dynamic Viewport Units

I’ve started sprinkling these in my styles since they gained wide support at the end of 2022. Like style queries, I only see limited use cases — most notably when setting elements to full height on a mobile device. So, instead of using height: 100vh, I’m starting to write height: 100dvh more and more. I guess that has influenced how I write CSS, even if it’s subtle.

Media Query Range Syntax

Honestly, I just haven’t thought much about the fact that there’s a nicer way to write responsive media queries on the viewport. I’m aware of it but haven’t made it a part of my everyday CSS for no other reason than ignorance.

OKLCH/OKLAB Color Spaces

oklch() will most definitely be my go-to color function. It gained wide support in March of this year, so I’ve only had a couple of months and no projects to use it. But given the time, I expect it will be the most widely used way to define colors in my CSS.

The only issue with it, I see, is that oklch() is incompatible with another color feature I’m excited about…

color()

It’s widely supported now, as of May 2023! That’s just too new to make its way into my everyday CSS, but you can bet that it will. The ability to tap into any color space — be it sRGB, Display P3, or Rec2020 — is just so much nicer than having to reach for a specific color function, at least for colors in a color space with RGB channels (that’s why color() is incompatible with oklch() and other non-RGB color spaces).

--primary-color: color(display-p3 1 0.4 0);

I’m not in love with RGB values because they’re tough to understand, unlike, say, HSL. I’m sure I’ll still use oklch() or hsl() in most cases for that very reason. It’s a bummer we can’t do something like this:

/* 👎 */
--primary-color: color(oklch 70% 0.222 41.29);

We have to do this instead:

/* 👍 */
--primary-color: oklch(70% 0.222 41.29);

The confusing thing about that is it’s not like Display P3 has its own function like OKLCH:

/* 👎 */
--primary-color: display-p3(1 0.434 0.088);

We’re forced to use color() to tap into Display P3. That’s at odds with OKLCH/OKLAB, where we’re forced to reach for those specific functions.

Maybe one day we’ll have a global color() function that supports them all! Until then, my CSS will use both color() and specific functions like oklch() and decide which is best for whatever I’m working on.

I’ll also toss color-mix() in this bucket, as it gained full support at the same time as color(). It’s not something I write regularly yet, but I’ll certainly play with it, likely for creating color palettes.

Honorable Mentions

It would be quite a feat to comment on every single new CSS feature that has shipped over the past five or so years. The main theme when it comes to which features I am not using in my day-to-day work is that they are simply too new or they lack browser support. That doesn’t mean I won’t use them (I likely will!), but for the time being, I’m merely keeping a side-eye on them or simply having a fun time dabbling in them.

Those include:

  • Trigonometric functions,
  • Anchor position,
  • Scroll-linked animations,
  • initial-letter,
  • <selectmenu> and <popover>,
  • View transitions,
  • Scoped Styles.

What about you? You must be writing CSS differently now than you were five years ago, right? Are you handling the cascade differently? Do you write more vanilla CSS than reaching for a preprocessor? How about typography, like managing line heights and scale? Tell me — or better yet, show me — how you’re CSS-ing these days.

Smashing Editorial (yk, il)