Container Queries Shift Responsive Thinking

Media queries never felt quite right for component-level responsive design. A component’s behavior depends on the space it actually occupies, not the viewport width, so juggling breakpoints across many components in the same viewport always involved compromise. Container queries solve that by letting child elements respond to their parent container’s dimensions.

Setting up a container is straightforward: define a selector as a container-type, and child elements can query that container’s size. This feels natural to CSS’s property-value model — no pre-calculating breakpoints or nesting media queries with preprocessor wrappers just to keep related styles together.

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

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

Media queries still have a place for page-level layouts that assemble multiple containers, where viewport-based breakpoints are more predictable. But the granular control container queries offer means less fighting with arbitrary breakpoints for individual components.

Cascade Layers for Organized Overrides

Cascade layers change how styles interact with the cascade by grouping rulesets into named layers with explicit priority order. Wrapping a reset or framework CSS in a layer keeps it tucked behind any un-layered styles, since un-layered rules take precedence over layered ones.

For single-file demos, one layer for base styles is enough. Demo-specific rules stay un-layered (and therefore higher priority), keeping them readable and easy to override while the reset and base styles stay neatly tucked away.

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

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

Production projects often still rely on Sass partialization for organization, which layers don’t necessarily replace. But for isolated snippets and experiments, layers offer a clean way to separate base styles from custom logic.

:is() and :where() Control Specificity

These relational selectors work well for their intended purpose, but their real value in daily CSS is specificity management. With :is(), the specificity of the entire selector equals that of the most specific argument in its list. :where(), by contrast, always contributes zero specificity regardless of its arguments.

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

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

That gives precise control. Need a ruleset to win? Wrap the selector in :is() to boost its score. Need global styles that anything can override? :where() keeps them effectively weightless.

A practical use case is containing :not() inside :where() so the negation selector doesn’t inadvertently raise specificity:

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

This also eases naming pressure. BEM class names describe element function and relationships, which gets hard to maintain. Lower-stakes specificity means element selectors become viable for many cases, reducing reliance on elaborate naming conventions.

Modern Color Function Syntax

The revised syntax for rgb() and hsl() removes friction that prompted unnecessary function-name choices. The awkward question of whether to use rgba() vs. rgb() whenever opacity is needed disappears — alpha is now just a /-separated argument.

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

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

That means one syntax, always. The newer oklch() and oklab() color spaces, now fully supported, extend this even further for perceptually uniform color manipulation.

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

Motion and Display Preferences Come First

Preference media queries made dark/light theming practical. But animation is where these queries have the most impact on workflow. The first consideration for any animation-related work is prefers-reduced-motion — not disabling motion entirely, but honoring the user’s choice for less movement.

: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);
}

A common approach is slowing transitions down significantly rather than eliminating them:

@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;
    }
  }
}

These preference queries, along with container queries, cascade layers, and modern selector and color syntax, make CSS more expressive without overhauling fundamentals. The basics — specificity, the cascade, responsive behavior — still apply, but the tools to manage them are far more capable than they were a few years ago.

Color Systems, Abstracted Further

My approach to color variables hasn't changed much since the Sass days: generic palette names feed into functionally named custom properties. What has changed is how much further I’m willing to abstract when a project calls for it. On larger palettes with many similar hues, pulling in color functions to manipulate values at a finer grain is worth the extra indirection.

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

/* Brand Colors */
--color-primary: var(--red);
/* etc. */
: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);
}
It may be possible to get similar control from color-mix(), which is something I haven’t fully explored yet. For now, the function-based approach handles the heavy lifting where dozens of distinct reds, yellows, and oranges need to stay consistent. ## Still on the Shelf For all the new features that have shipped, the list I actually reach for daily is short. Most of what I’m skipping is either too fresh for browser support or simply not yet a habit. The feature set I use hasn’t changed dramatically; rather, a long tail of newer capabilities is waiting in the wings. **CSS Nesting.** Native nesting might finally be the reason I drop Sass entirely. It’s still awaiting Firefox support at the time of writing, so my preprocessor isn’t going anywhere just yet. **Style Queries.** Applying styles based on another element’s computed styles is conceptually appealing, but concrete use cases are still hard to pin down. Broader support and more experiments from the community may change that. **:has().** The parent selector is great in theory, but without Firefox support I’m only tinkering. Once it’s everywhere, selecting a parent by its children will certainly reshape how I structure CSS. **Dynamic Viewport Units.** Units like 100dvh have found their way into my stylesheets since late 2022. The use case remains narrow—mostly full-height layouts on mobile—but the switch from 100vh is a subtle change I’ve already made. **Media Query Range Syntax.** The newer, more expressive range syntax hasn’t become part of my regular routine. Not for any technical reason—it’s simply a case of not having integrated it into my daily thinking. **OKLCH/OKLAB.** Since wide support landed in March, oklch() has become my preferred color function. I haven’t yet worked on a project where I could use it, but given the opportunity, I expect it to dominate my palette definitions. The catch is that it doesn’t play nicely with another feature I’m keen on. **color().** This function gained broad support in May 2023, which is too recent for regular use. Accessing any RGB-based color space—sRGB, Display P3, Rec2020—is far more flexible than reaching for separate functions per color space. The inherent limitation is that non-RGB spaces like OKLCH are incompatible.
--primary-color: color(display-p3 1 0.4 0);
RGB values never felt intuitive to me the way HSL did, so in practice I’ll still prefer oklch() or hsl(). It’s unfortunate that we can’t write something like this:
/* 👎 */
--primary-color: color(oklch 70% 0.222 41.29);
Instead, the syntax requires:
/* 👍 */
--primary-color: oklch(70% 0.222 41.29);
What’s confusing is that Display P3 doesn’t have its own dedicated function the way OKLCH does:
/* 👎 */
--primary-color: display-p3(1 0.434 0.088);
That forces color() into service for Display P3, while OKLCH demands its own function. Ideally, a universal function would handle every color space, but until then I’ll use whichever fits the task. color-mix() falls into the same category: it shipped alongside color() and is still more of a palette-building experiment than a daily tool. **Honorable Mentions.** A full survey of every new CSS capability is beyond scope. The common thread is that features I haven’t adopted are either very new or blocked by browser coverage. I’m paying attention to a few, but haven’t committed to them yet: - Trigonometric functions, - Anchor position, - Scroll-linked animations, - initial-letter, - <selectmenu> and <popover>, - View transitions, - Scoped styles. So, are you writing CSS differently than you were a few years back? Have you changed how you manage the cascade, written more vanilla CSS than preprocessed, or revamped your typography approach? I’d be interested to hear—or better, see—how your current CSS workflow has evolved.