DEV Community

Cover image for SCSS Is Solving Problems CSS Is Learning to Solve Itself
Olexandr Uvarov
Olexandr Uvarov

Posted on

SCSS Is Solving Problems CSS Is Learning to Solve Itself

You've spent years reaching for SCSS the moment a project grows past a single stylesheet. Variables. Loops. Functions. Nesting. It felt like the only sane way to write scalable CSS.

Meanwhile, native CSS has been quietly shipping the same features — one by one, browser by browser.

So here's the honest question: how much of SCSS do you actually still need?

Let me walk through what CSS can do natively today, what's coming next, and where SCSS still wins.


What CSS Can Do Right Now

Custom Properties (aka CSS Variables)

You already know these. They replaced SCSS $variables for runtime use-cases — and they're strictly more powerful because they cascade, they're dynamic, and JavaScript can interact with them.

:root {
  --color-primary: #ff69b4;
  --spacing-base: 8px;
}

.button {
  background: var(--color-primary);
  padding: calc(var(--spacing-base) * 2);
}
Enter fullscreen mode Exit fullscreen mode

SCSS variables are build-time only. Custom properties survive into the browser. If you haven't fully made the switch for your design tokens, that's where to start.

Nesting

Native CSS nesting shipped in Chrome 112 (2023) and is now in every major browser. The syntax is almost identical to SCSS:

.card {
  padding: 16px;

  &__title {
    font-size: 18px;
  }

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }

  @media (max-width: 768px) {
    padding: 12px;
  }
}
Enter fullscreen mode Exit fullscreen mode

Two differences worth knowing before you migrate:

  1. & is mandatory in native CSS. .card h1 {} nested inside .card {} won't work — you need & h1 {}.
  2. No interpolation. SCSS lets you write &#{$modifier} to build dynamic selectors. Native CSS nesting has no equivalent — those cases still need a preprocessor.

For most codebases, neither matters day-to-day. But if you're running BEM with dynamic modifiers or generating selectors in loops, you'll hit the wall.

color-mix() and Color Functions

SCSS has lighten(), darken(), mix(). Native CSS now has color-mix():

.button-hover {
  background: color-mix(in oklch, var(--color-primary) 80%, white);
}
Enter fullscreen mode Exit fullscreen mode

The in oklch part tells the browser which color space to interpolate in. This matters more than it sounds: mixing colors in srgb (the default) can produce muddy mid-tones. oklch is perceptually uniform — meaning the lightness you see actually matches the lightness you specified. Tints and shades look consistent instead of just mathematically being consistent.

Supported in all modern browsers. Works with CSS custom properties, which SCSS color functions don't.

@layer

This is the one CSS feature that genuinely has no SCSS equivalent because it solves a problem SCSS can't even address: specificity wars at scale.

Quick context if you haven't lived through one: you import a third-party component library. Its .button styles have higher specificity than yours. You add !important. They update the library and add !important too. Now you're writing !important !important in your head and nothing is predictable. @layer fixes this at the architecture level.

@layer reset, base, components, utilities;

@layer reset {
  * { box-sizing: border-box; }
}

@layer components {
  .button { background: var(--color-primary); }
}

@layer utilities {
  /* This ALWAYS wins over components, regardless of specificity */
  .mt-4 { margin-top: 16px; }
}
Enter fullscreen mode Exit fullscreen mode

Layer order controls override priority — not specificity, not source order, not !important. Later-declared layers win. If you're building a design system that external teams can extend without fighting the cascade, this is the tool.

Container Queries

For years, the argument was "CSS can't do what JavaScript can do for responsive design." Container queries close that gap:

.card {
  container-type: inline-size;
}

@container (min-width: 400px) {
  .card__title {
    font-size: 24px;
  }
}
Enter fullscreen mode Exit fullscreen mode

Components that adapt to their parent container, not the viewport. Media queries were always the wrong abstraction for component libraries — this is the right one.


What's Coming Next (and When to Care)

⚠️ Everything in this section is not yet in browsers (or only behind experimental flags). The syntax shown is from active CSS Working Group specs — real and moving forward, but not production-ready. If you want to use any of this today, PostCSS has polyfill plugins for most of it. More on that at the end.

@function — Native CSS Functions

This is the big one. The CSS Working Group has a specification in progress for custom functions directly in CSS. No JavaScript, no SCSS:

/* Proposed syntax — not in browsers yet */
@function --spacing($multiplier) {
  result: calc(8px * $multiplier);
}

.card {
  padding: --spacing(3); /* 24px */
}
Enter fullscreen mode Exit fullscreen mode

This would replace one of SCSS's most-used features. Timeline: likely 2025-2026 for an experimental flag, broader support a year or two after that.

@mixin — Native CSS Mixins (sort of)

Also in the CSS Mixins & Functions spec. The concept: reusable blocks of declarations, applied without a preprocessor:

/* Proposed syntax */
@mixin --flex-center {
  display: flex;
  align-items: center;
  justify-content: center;
}

.hero {
  @apply --flex-center;
}
Enter fullscreen mode Exit fullscreen mode

Important caveat for anyone who's used SCSS mixins heavily: this is a significantly simpler model. No @content injection. No arguments with conditional logic. No @if inside the mixin body. It's closer to a named block of static declarations than a parametric function. Think of it as CSS @extend done right — not SCSS @mixin done natively. The name is similar; the power level is not.

if() — Inline Conditionals

CSS is getting inline conditional logic as part of the Values Level 5 spec:

.button {
  /* Adjust padding based on a custom property flag */
  padding: if(style(--size: large): 20px 32px; else: 12px 20px);
}
Enter fullscreen mode Exit fullscreen mode

Combined with custom properties as flags, this lets components adapt their own styles based on configuration — no JavaScript, no modifier classes, no SCSS conditionals.

Relative Color Syntax

Already shipped in Chrome and Safari, making its way to Firefox:

.button-hover {
  /* Take primary color, keep h and s, reduce l by 10% */
  background: hsl(from var(--color-primary) h s calc(l - 10%));
}
Enter fullscreen mode Exit fullscreen mode

This replaces lighten() and darken() with something more predictable and composable. Works with custom properties, which SCSS functions don't.


Where SCSS Still Wins (For Now)

Being honest here — the answer isn't "CSS wins completely." There are three areas where SCSS still has a real advantage in 2025:

1. Loops and data-driven generation

@for, @each, @while — CSS has nothing equivalent yet. If you're generating 16 animation delays for a visualizer, or creating utility classes from a scale map, SCSS loops are still the only build-time option. The proposed CSS @function spec doesn't include iteration.

2. Build-time validation

SCSS @error and @warn fire at compile time. If your design token is missing, you find out before the browser renders anything. CSS has no equivalent concept — it fails silently.

@if not map-has-key($modes, $mode) {
  @error "Unknown mode: #{$mode}. Available: #{map-keys($modes)}";
}
Enter fullscreen mode Exit fullscreen mode

There's no native CSS equivalent coming. This remains a preprocessor-only capability.

3. Complex structured data

SCSS maps as nested data structures — $modes: (pink-big: (title: (color: ..., font-size: ...))) — are genuinely expressive in ways CSS custom properties can't replicate. CSS properties are flat key-value. SCSS maps are traversable trees. For the kind of multi-mode theming system I wrote about in the first article in this series, SCSS maps as a config layer still make sense even when CSS handles the runtime switching.


The Middle Ground Nobody Talks About: PostCSS

The "SCSS vs native CSS" framing misses a third option that's already how many production setups work: PostCSS.

PostCSS sits between your source files and the browser. It's not a preprocessor like SCSS — it's a transformation pipeline. You write future CSS syntax, PostCSS compiles it to what browsers support today.

The practical upside: you can use @custom-media, CSS nesting (even before it was fully shipped), relative color syntax, and some of the proposed @function syntax right now via PostCSS plugins. Vite has PostCSS built in. Next.js does too.

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-nesting'),          // native nesting polyfill
    require('postcss-custom-media'),     // @custom-media queries
    require('postcss-oklab-function'),   // oklch/oklab color support
  ]
}
Enter fullscreen mode Exit fullscreen mode

This is worth knowing because it changes the timeline question. You don't have to wait 2-3 years for @function to ship in browsers — you can write it today if PostCSS has a plugin for it. The CSS ecosystem is converging on this model: specify in CSS, transform at build time, no preprocessor lock-in.

The Risks Nobody Mentions

Going native CSS isn't just a technical decision — it's a bet on your users' browsers. Here's what can go wrong.

You don't actually know what browsers your users run

"All modern browsers support it" is a statement about the latest versions. Your users might not be on the latest versions.

The gap is bigger than you think:

  • Corporate users on locked-down Windows machines often run Chrome that's 6-12 months behind
  • Android WebView (used inside apps, not the standalone browser) updates on its own schedule and frequently lags
  • Safari on iOS is tied to the OS version — users on iPhone 8 running iOS 15 are stuck on Safari 15, and iOS 15 was dropped from Apple's update cycle in 2022
/* CSS nesting — ~93% global support */
/* container queries — ~91% global support */
/* color-mix() — ~89% global support */
/* @layer — ~92% global support */
/* relative color syntax — ~86% global support */
Enter fullscreen mode Exit fullscreen mode

That 7-14% that doesn't support it isn't a rounding error. If you have 500,000 users, that's 35,000–70,000 people seeing broken styles — silently, with no error in your logs.

CSS fails silently, SCSS fails loudly

This is the asymmetry that matters most in production.

When an SCSS build fails, your CI pipeline fails. The broken code never ships. With native CSS, the browser just skips the declaration it doesn't understand and moves on:

.card {
  /* Browser doesn't understand container queries → skips silently */
  container-type: inline-size;
  /* Falls back to... nothing. No fallback defined. Layout breaks. */
}
Enter fullscreen mode Exit fullscreen mode

No error. No warning. No alert. You find out when a user screenshots a broken UI and posts it somewhere.

SCSS gave you build-time safety. Native CSS gives you runtime silence.

The progressive enhancement tax

The correct answer to browser support gaps is progressive enhancement — define a baseline that works everywhere, then layer on the modern feature:

.card__title {
  /* Baseline: works everywhere */
  font-size: 18px;
}

/* Enhancement: only where container queries work */
@container (min-width: 400px) {
  .card__title {
    font-size: 24px;
  }
}
Enter fullscreen mode Exit fullscreen mode

This is the right pattern. But it's also more code, more cognitive load, and more surface area to test. With SCSS you compiled one output and called it done. With native CSS + progressive enhancement you're maintaining two layers of intent for every feature you use. It's manageable. But it's not free.

@supports helps — but only partially

Unlike JavaScript where you can if ('IntersectionObserver' in window), CSS has no clean runtime feature detection. @supports gets you partway there:

@supports (container-type: inline-size) {
  .card { container-type: inline-size; }
}

@supports not (container-type: inline-size) {
  /* fallback layout */
}
Enter fullscreen mode Exit fullscreen mode

But @supports checks syntax, not behavior. Some older browsers will claim to support a feature and implement it incorrectly. You don't find out until QA on a real device — or until a user reports it.

The practical checklist before going SCSS-free

Before committing to native-CSS-only on a project, answer these four questions:

1. What does your analytics say about browser versions?
If you have >5% traffic on browsers older than 2 years, you need fallbacks for almost everything in this article.

2. Do you test on real devices?
Chrome DevTools device emulation is not the same as a real Android 10 device. Android WebView lags behind Chrome proper and is where you'll catch the most rendering bugs — but only if you actually test there.

3. What's your fallback strategy when a feature isn't supported?
"It probably works" isn't a strategy. Define explicitly: if @layer doesn't load, what does the user see? If container queries don't fire, does the layout degrade gracefully or snap into something broken?

4. Does your team have the CSS depth to debug this?
SCSS errors are compiler errors — they're explicit. Native CSS failures are visual regressions with no stack trace. You need someone on the team who can open DevTools, spot a silently-ignored declaration, and trace it back to a support gap. That's a different skill than reading a build log.


The Honest Answer to "Can We Drop SCSS?"

For new projects starting today: probably yes, with caveats. Native nesting, custom properties, color-mix(), container queries, and @layer handle the majority of what SCSS used to be required for. You'd miss loops and build-time validation.

For existing projects: almost certainly no, not yet. The migration cost isn't justified by the current feature delta. The gap is closing, but it's not closed.

The direction is clear though. Every SCSS feature that can be replicated natively will be replicated natively — that's been the pattern for ten years. Variables came first. Nesting followed. Functions and mixins are in spec. The browser is slowly absorbing the preprocessor.

The question isn't whether SCSS will become optional — it's when. My guess: two to three years before native CSS covers enough ground that starting a new project without a preprocessor stops feeling like you're handicapping yourself.

Until then, the smart play isn't "all SCSS" or "all native CSS." It's knowing which problems each solves better right now — and not reaching for a build tool out of habit when the platform handles it.


The real question isn't which individual feature you'd miss — it's whether you'd lose a workflow. Build-time validation, structured data as config, loop-driven generation: these aren't features you replace one-for-one, they're patterns. What pattern from your SCSS setup do you think CSS will never cover natively? Drop it in the comments.

Top comments (0)