CSS Preprocessor Features Cheat Sheet
If you have used SCSS or Less, you know variables, nesting, and mixins as preprocessor features. Native CSS now offers all three: custom properties (`--var`), the CSS Nesting Module, and `@layer`. This snippet shows the SCSS form alongside the native form for each so you can see when you actually need a preprocessor and when plain CSS is enough. Audience: practitioners moving from SCSS to native CSS and wondering what they get to drop.
967 views
21
/* SCSS */
/* SCSS variables are compile-time substitutions. They cannot be changed at
runtime, cannot be inspected in DevTools, and cannot vary by selector scope. */
/*
$primary: royalblue;
$radius: 6px;
.button {
background: $primary;
border-radius: $radius;
}
*/
/* Native CSS */
/* Custom properties are runtime values. They live in the cascade, can be
redefined per selector, and JavaScript can read or write them. */
:root {
--primary: royalblue;
--radius: 6px;
}
.button {
background: var(--primary);
border-radius: var(--radius);
}
/* Scoped override: a card with a different accent color, no extra class needed
on every nested element. SCSS variables cannot do this. */
.card.danger {
--primary: crimson;
}
.card.danger .button {
/* Picks up the locally-redefined --primary automatically. */
background: var(--primary);
}SCSS variables are resolved by the compiler before the browser ever sees the CSS, which makes them feel like constants in a programming language. Native custom properties are part of the cascade, which is far more powerful: redefining --primary on .card.danger automatically flows down to every descendant that reads var(--primary), with no extra selector multiplication. Custom properties also let JavaScript do element.style.setProperty('--primary', '#fff') for runtime theming. The one place SCSS variables still win is loops and arithmetic at build time; for everything else, native is simpler and more powerful.
/* SCSS */
/* SCSS has had nesting since 2006. The & references the parent selector. */
/*
.menu {
font-size: 1rem;
.item {
padding: 0.5rem 1rem;
&:hover {
background: #f0f0f0;
}
&.active {
font-weight: 700;
}
}
}
*/
/* Native CSS */
/* The CSS Nesting Module (broadly supported since late 2023) brings the same
syntax to the browser with no compiler. */
.menu {
font-size: 1rem;
.item {
padding: 0.5rem 1rem;
&:hover {
background: #f0f0f0;
}
&.active {
font-weight: 700;
}
}
}
/* The & is REQUIRED in native CSS when the nested selector starts with a
pseudo-class, attribute, or element selector; SCSS allowed it implicitly,
native CSS does not. Always use & to be safe. */
.toggle {
background: white;
/* Native: must write &:checked, not :checked. */
&:checked {
background: dodgerblue;
}
}Native CSS nesting reads almost identically to SCSS: indent a selector inside a parent rule and use & to reference the parent. The big behavioral difference is that native CSS requires the & in more places than SCSS did. Specifically, when the nested selector starts with a type, pseudo-class, attribute, or pseudo-element selector, you must write &:hover and not just :hover; SCSS treated bare :hover as & :hover (descendant), then changed to &:hover (concatenated) in newer versions, which always confused readers. The native rule is consistent: use & whenever you mean "the same element with this state". Browser support is good across modern engines, but ship a static CSS fallback if you must support old Safari.
/* SCSS */
/* Mixins encapsulate reusable declarations and accept arguments. */
/*
@mixin button-base($bg, $fg: white) {
background: $bg;
color: $fg;
padding: 0.5rem 1rem;
border: 0;
border-radius: 4px;
}
.btn-primary {
@include button-base(royalblue);
}
.btn-danger {
@include button-base(crimson, white);
}
*/
/* Native CSS */
/* Native @function and @mixin proposals exist but are not widely shipped yet.
The closest stable equivalent today is a custom-property bundle plus a base
class that consumers extend. */
.btn-base {
background: var(--btn-bg, royalblue);
color: var(--btn-fg, white);
padding: 0.5rem 1rem;
border: 0;
border-radius: 4px;
}
.btn-danger {
--btn-bg: crimson;
}
/* @layer (Cascade Layers) is the native answer to "how do I keep utility
classes from accidentally beating my framework styles". Layers declared
first lose to layers declared later, regardless of selector specificity. */
@layer reset, framework, components, utilities;
@layer reset {
* { box-sizing: border-box; }
}
@layer framework {
.btn-base {
background: var(--btn-bg, royalblue);
color: var(--btn-fg, white);
}
}
@layer utilities {
.text-center { text-align: center; }
}SCSS mixins and Less mixins compile to inlined declarations, which makes them feel like template functions. Native CSS does not yet have a shipped @mixin, but the practical replacement is a base class plus custom-property variants: define defaults via var(--name, fallback) in the base, then have variant classes set the variables. For specificity management, native CSS now offers @layer, which is more powerful than anything SCSS provides: layers ordered later win regardless of how specific the selector is, so a utility class always beats a framework class without ever needing !important. Use SCSS today for arithmetic, color functions, and complex loops; reach for native CSS for variables, nesting, and layered cascade control.
