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.
-
Architecture That Scales
-
Modern CSS Without a Framework Crutch
-
Cascade Layers and Specificity You Control
-
-
-
Layout Systems for Real Sites
-
Complex Grids and Nested Layout Patterns
-
Container Queries When the Component Is the Breakpoint
-
-
-
Motion and Interaction Done Right
-
Transitions and Animations That Don't Annoy People
-
prefers-reduced-motion and Respecting the User
-
-
-
Performance and Delivery
-
Critical CSS and What Blocks First Paint
-
Font Loading Without Layout Jump
-
Responsive Images: srcset, sizes and Lazy Load
-
-
-
Ship and Maintain
-
Design Tokens and Theming Without Duplicating Everything
-
CSS That Survives a Redesign
-
Audit Accessibility and Visual Regressions Before Launch
-
-
- Final Check: HTML and CSS Advanced
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 did | What you write instead |
|---|---|
| Spacing and type scale | Custom properties and clamp() |
| Responsive grid | display: grid, gap and minmax() |
| Scoped component rules | Native 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 text | Logical 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.cardor.btnclass 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
Comments (0)
Sign in to commentNo comments yet.