A dense bundle of tangled, multicoloured electrical wires

Modern CSS Migrations: Why Fewer Lines Can Mean More Complexity

Deleting a script and replacing it with a few lines of CSS is one of the most satisfying pull requests a developer can open. Container queries, :has() and native nesting now make that swap possible for all sorts of components. But a shorter file isn’t the same as a simpler website, and the problems tend to show up weeks later in places nobody touched.

Hand-painted wireframe sketches of card-based web page layouts in blue, purple and green

One Component, Two Bosses

Imagine a card that goes two-column above 480px. The old version used a ResizeObserver to add a class; the new one uses a container query.

/* New: no script needed */
.card-wrap { container-type: inline-size; }

@container (width > 480px) {
  .card__body { display: grid; grid-template-columns: 10rem 1fr; }
}

/* Leftover from the old approach, still in another file */
.card--wide .card__title { font-size: 1.5rem; }

If the observer script survives the migration, both systems keep running. Everything looks fine until someone moves the breakpoint in one place only, and the card ends up with a narrow body and an oversized heading. The rule of thumb: let CSS own presentation, let JavaScript own application state, and delete every trace of the old approach, including the class and any selectors that use it.

The Side Effects of container-type

Turning an element into a query container does more than enable queries. It adds containment and creates a new block formatting context, which means:

Go Ad Free
  • child margins no longer collapse through it, so spacing can suddenly grow;
  • it sits beside floats instead of wrapping round them;
  • if its width came from its content (a flex item, inline-block or fit-content box), it can shrink to 0px and disappear.
A laptop and smartphone on a desk showing the same web interface at different sizes

The safest fix is usually to put the container on a wrapper that already gets its width from the layout, then test the component in every real place it’s used.

Selectors That Reach Too Far

:has() matches more than you think

A rule meant for “cards with a hero image” will also fire when a comment inside the card has an avatar, and because :has() takes the specificity of its argument, it can override the comment’s own styles. Child combinators keep it in check:

/* Too broad: any image, any paragraph */
.card:has(img) p { margin: 0; }

/* Focused: direct children only */
.card:has(> img) > p { margin: 0; }

Nesting can borrow an ID’s weight

A nested & takes the highest specificity in its parent list, so nesting under .card, #featured gives every child rule ID-level weight. Wrapping the list in :where() drops that to zero:

:where(.card, #featured) {
  & a { color: navy; }
}

Cascade layers won’t save you by default either: unlayered styles beat layered ones, so new layered components lose every clash with an old unlayered stylesheet.

Is It Really Faster?

Moving work into CSS hands it to the browser’s style engine; it doesn’t make it disappear. In one recent Chrome test on a 5,000-row list, two :has() rules that were each cheap on their own combined to restyle every row on each update, taking roughly 7ms, and around 31ms at 20,000 rows. That was one browser on one machine, but the lesson stands: measure with realistic data in your browser’s performance panel before and after, rather than assuming.

A designer sketching a website wireframe in a notebook on a dark desk

Questions to Ask Before You Merge

Who owns this behaviour now?

Exactly one of CSS or JavaScript should. If both still touch it, the migration isn’t finished.

Go Ad Free

What else does this change affect?

Check the element’s width, its parent, its children and its neighbours, especially after adding container-type.

Which rules could win or lose against it?

Look at specificity, nesting and layers, not just the rule you wrote.

Has the old code really gone?

Search for the old class names and scripts, then test every state the component can be in.

Did you measure it?

Compare style, layout and paint time before and after, using production-sized content.

Modern CSS is a genuine step forward, and none of this is a reason to avoid it. For business owners, it’s a reminder that a “quick tidy-up” of a website’s code deserves proper testing, especially on older sites that have grown over the years.

Go Ad Free

Create a free account, or log in.

Gain access to read this article, join developers who build better, faster, smarter solutions

Yes! I would like to receive new content and updates.

Scroll to Top