Skip to content
2 min read421 words

The case for boring CSS

Why I keep reaching for the oldest trick in the book instead of the newest framework, with a concrete audit of a page that shipped 40kb lighter because of it.

There is a version of frontend development where every problem gets solved by adding a dependency. Scroll animations? Install a library. Complex layout? Install a library. A dropdown? Install a library.

I did that for years. I now think the boring path is almost always the better one, and I have a fairly specific definition of boring.

What "boring" means here

Boring CSS is CSS that has been in every browser since roughly 2015 and will be in every browser until roughly 2100:

  • grid and flexbox for layout
  • clamp() for fluid type
  • position: sticky for the header
  • aspect-ratio to reserve space
  • Custom properties for theming
  • prefers-reduced-motion for sanity

No build step, no runtime, no polyfill. It renders the same on the server and the client.

A concrete audit

Take a product page. The brief called for a sticky header, a horizontally scrolling gallery, a sticky "buy" rail on desktop, and staggered fade-ins on scroll.

First attempt would have been roughly:

  • A scroll-animation library
  • A carousel library
  • A sticky positioning polyfill
  • A class-name utility layer with a build plugin

What shipped instead:

css
.sticky-rail {
  position: sticky;
  top: 5rem;
}
 
/* Fluid type without a single media query */
.headline {
  font-size: clamp(1.75rem, 1rem + 3vw, 3.5rem);
  line-height: 1.05;
  text-wrap: balance;
}
 
/* Horizontal scroll, done by the browser */
.gallery {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(22rem, 78vw);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
}
 
.gallery > * {
  scroll-snap-align: center;
}

Native scroll snap is a better carousel than the library version: momentum, keyboard support, touch physics, and accessibility all come free from the platform.

For the fade-ins, the boring answer is the one people find unsatisfying: don't.

css
@media (prefers-reduced-motion: no-preference) {
  .reveal { animation: rise 0.4s steps(6, end) both; }
}

If an element animates in on scroll, the user has already scrolled, which means they were in a hurry. Animating at them is a small tax on the most impatient visitors.

The result: 40kb lighter, no layout shift, better a11y, and less code to maintain.

Where libraries earn their keep

I'm not absolutist. Some things genuinely should not be hand-rolled:

  • Date parsing and formatting. Intl is good but the edge cases are endless.
  • Complex data tables. Virtualisation, sorting, sticky headers, keyboard nav.
  • Rich text editing. Do not write an editor.
  • Anything with a spec. WebSocket protocols, media codecs.

The rule I'd propose: if it's a layout or styling problem, solve it with CSS. If it's a data or protocol problem, use a library.

The test

Before adding a dependency, I ask: what is the browser already doing that I would be reimplementing?

The answer is usually more than you'd expect. Scroll snapping. Stacking contexts. content-visibility: auto. View transitions. Container queries. :has(). The gap selector.

A lot of frontend performance work is really just a willingness to use features that shipped after your last conference talk.

Filed under MAY 21 '26

All posts →

Command palette

Search for a command to run