Skip to Content
HTML and CSS Advanced: Styles That Hold Up in Production
Advanced
0 likes
Courses
HTML and CSS Advanced: Styles That Hold Up in Production

Modern CSS Without a Framework Crutch

Replace a reset, a spacing scale and a grid with native features like nesting, :where(), custom properties and Grid, and see where a framework still earns its place. After this, you can decide on purpose instead of by habit.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If you already write your own base stylesheet with native nesting, :where(), custom properties and clamp(), skip to "Where a framework still earns its place", then continue.

A CSS framework is a reasonable choice. It is a bad default. Most projects pull in a whole framework for four jobs: a reset, a spacing scale, a layout grid and some component states. The browser can do all four by itself now. When you keep the framework anyway, you inherit its class names, its specificity and its upgrade schedule, and you pay for that every time you override something.

What the platform gives you

Job the framework didWhat you write instead
Spacing and type scaleCustom properties and clamp()
Responsive griddisplay: grid, gap and minmax()
Scoped component rulesNative CSS nesting with &
Defaults that are easy to override:where(), which adds zero specificity
Styling a parent by what it contains:has()
Spacing that survives right-to-left textLogical properties like margin-block

Container queries, :has(), subgrid, nesting and cascade layers are all supported in current Chrome, Edge, Firefox and Safari, so none of this needs a build step.

A small base stylesheet

:root {
  --space-1: 0.5rem;
  --space-2: 1rem;
  --space-3: 2rem;
  --text: #080b0e;
  --accent: #034bff;
}

*, *::before, *::after { box-sizing: border-box; }

:where(body) { margin: 0; color: var(--text); font: 1rem/1.5 system-ui, sans-serif; }
:where(h1, h2, h3) { line-height: 1.2; text-wrap: balance; }
:where(img, video) { max-width: 100%; height: auto; display: block; }

.stack > * + * { margin-block-start: var(--space-2); }

.grid {
  display: grid;
  gap: var(--space-2);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

.card {
  padding: var(--space-2);
  border: 1px solid color-mix(in srgb, var(--text) 25%, white);
  border-radius: 8px;

  & h2 { margin-block: 0 var(--space-1); font-size: clamp(1.1rem, 1rem + 1vw, 1.5rem); }
  &:has(img) { padding-block-start: 0; }
  &:hover { border-color: var(--accent); }
}

I ran this in Chrome 154. Three cards land in three 312px columns at a 1000px viewport. At a 320px viewport the grid becomes one 288px column with no horizontal scroll, because min(100%, 16rem) stops the column from being wider than its container. The card heading came out at 24px at 1000px wide, which is the 1.5rem cap of the clamp().

Why each piece is there

  • :where() wraps the element defaults. A plain .card or .btn class beats them, so you never fight your own reset.
  • .stack > * + * puts space between siblings and nowhere else. The parent owns the spacing, not each child.
  • Nesting with & keeps the card rules in one block. It is not a license to go five levels deep.
  • :has(img) lets a card react to what it contains, with no extra class and no JavaScript.

⚠ Warning: Do not rip a framework out of a live project in one commit. Replace one component at a time and delete the framework's rules only after the page looks the same without them.

Where a framework still earns its place

A prototype due this week. A team with no designer. A design system you did not write and have to match. A set of widgets, such as date pickers and dropdowns, that ship with behaviour and not just styles. I cannot tell you which of those describes your project. I can tell you the choice should be on purpose, not because nobody ever asked.

Common mistakes

  • Treating "no framework" as "no structure". One 3,000-line file with no layers or naming is worse than the framework.
  • Nesting four levels deep because the syntax allows it. The selector the browser has to match is as heavy as one you wrote by hand.
  • Putting :where() on component rules as well, so nothing has a predictable winner.

💡 Tip: Start with the reset and the tokens. Add a layout helper only when the second page needs it, not before.

What happens if you skip this

You keep shipping a whole framework to use a small part of it. Every override becomes a longer selector or an !important. A rebrand then means fighting someone else's variables.

Try it

Take one page that uses a framework grid. Rebuild only the grid with the .grid rule above, load the page, then open DevTools Coverage and see how much of the framework stylesheet that page actually used.

If most of it went unused, what exactly are you keeping it for?

About

Replace a reset, a spacing scale and a grid with native features like nesting, :where(), custom properties and Grid, and see where a framework still earns its place. After this, you can decide on purpose instead of by habit.

Duration
5 min
Last update
08/15/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.