Vivian Voss

Vanilla CSS: The Sass Replacement

css web tooling

The Replacement ■ Episode 04

“Sass was the bridge. The bridge has arrived.”

The year is 2012. CSS is a specification written by committee and read by nobody. It has no variables, no nesting, no functions, and no mathematical operators beyond the kind you learn at age seven. Writing maintainable stylesheets at scale is an exercise in copy-paste, find-and-replace, and quiet desperation. Hampton Catlin and Natalie Weizenbaum release Sass, a preprocessor that adds everything CSS lacks: variables, nesting, mixins, functions, partials, and mathematical operations. It is brilliant. It is necessary. It changes how an entire generation writes stylesheets.

The year is 2026. CSS can do all of it. Natively. In the browser. Without a build step, without a Ruby gem, without a Node dependency, and without the biennial argument about whether to use node-sass or dart-sass and which one has broken this week.

The question is not whether Sass was good. It was. The question is whether it is still necessary. It is not.

The Feature Matrix

Let us be precise about what Sass provided in 2012 and what CSS provides natively in 2026. Not in theory. Not in a specification draft. In every browser your users actually run.

SASS 2012 vs NATIVE CSS 2026 Feature Sass (2012) CSS (2026) Variables $primary: #e65100 Dead after compile --primary: oklch(65% .22 45) Live at runtime, JS accessible Nesting .nav { a { color: red } } Preprocessor only .nav { a { color: red } } Native since Dec 2023 Colour darken($c, 10%) HSL, perceptually uneven oklch(from var(--c) calc(l - .1) c h) OKLCH, perceptually uniform Structure @import "partials" File concatenation @layer base, components, pages Cascade control, any order Maths math.div($w, 3) Build-time only calc(), min(), max(), clamp() Runtime, responsive, mixed units Mixins @mixin respond($bp) { ... } Reusable blocks with logic Custom properties + clamp() 95% of mixin use cases solved Every row: build-time workaround replaced by browser-native feature

The syntax for nesting is identical. Not similar. Identical. The specification was deliberately written to match Sass’s syntax so that migration would be mechanical. The CSS Working Group, in a rare act of pragmatic mercy, chose compatibility over innovation. Browser support reached 87% by December 2023 and sits above 92% today.

Variables: The Living and the Dead

Sass variables die at compile time. You write $primary: #e65100, the preprocessor substitutes every occurrence, and the output is a static CSS file containing the resolved values. The variable itself ceases to exist. It is a search-and-replace macro with better syntax.

CSS Custom Properties are alive at runtime. --primary exists in the DOM. It can be read by JavaScript. It can be overridden per element, per media query, per :hover state. A single property declaration enables dark mode, theming, component-level customisation, and runtime colour manipulation, none of which a Sass variable can do, because a Sass variable is not there when the page loads. It was there during the build. Then it left.

The hex colour that Sass variables carry is its own anachronism. #e65100 encodes colour in a notation from 1996 that is neither perceptually uniform nor human-readable. It is, to borrow a phrase, the fax machine of colour notation. Modern CSS uses OKLCH, a perceptually uniform colour space where lightness actually means lightness, and adjusting one channel does not produce unexpected shifts in another. Sass’s darken() operates in HSL, where “10% darker” is a mathematical approximation that your eyes would politely dispute.

Structure: From Partials to Layers

Sass structured stylesheets through @import and partials. You split your CSS into files named with a leading underscore, imported them in a specific order, and the preprocessor concatenated them into a single output file. The order mattered. Get it wrong and your resets arrived after your components, your utilities trampled your layouts, and your specificity became a diplomatic incident.

CSS Cascade Layers solve this properly. With @layer, you define the cascade hierarchy in a single declaration: @layer reset, base, components, utilities. The order of the @layer rule determines priority, regardless of the physical order in which styles are loaded. Load your component styles before your resets. It does not matter. The layer order governs. The specificity war ends not with a victory but with a new constitution.

Maths: From Build-Time to Runtime

Sass provided arithmetic because CSS had none. You could divide widths, multiply spacing values, and calculate grid fractions at compile time. The results were baked into the output. Static. Immutable. Correct only for the viewport size that existed in the developer’s imagination.

CSS now has calc(), min(), max(), and clamp(), all evaluated at runtime, all capable of mixing units. clamp(1rem, 2.5vw, 2rem) creates fluid typography that responds to the viewport without a single media query or Sass @mixin. The arithmetic is not computed once and frozen. It is computed continuously, by the browser, for every device, at every viewport width, in real time. The preprocessor calculated an answer. The browser calculates the answer.

Mixins: The 95% Argument

The most honest defence of Sass in 2026 centres on mixins. Mixins allow reusable blocks of CSS with conditional logic, arguments, and loops. No native CSS feature replicates this capability directly.

The honest counter-argument is that 95 per cent of mixins in production codebases exist for three reasons, all of which CSS has since eliminated:

Vendor prefixes. The primary use of Sass mixins in the 2010s was generating -webkit-, -moz-, and -ms- prefixes. In 2026, the properties that required prefixes either no longer need them or are handled by a single line of PostCSS with Autoprefixer, itself increasingly unnecessary as browser convergence has reduced the prefix surface to a handful of edge cases.

Responsive breakpoints. The @mixin respond-to($bp) pattern wrapped media queries in a reusable function. Container queries, clamp(), and fluid type scales have made the majority of these mixins redundant. The component responds to its own context, not to an arbitrary viewport width defined in a Sass partial three directories away.

Property calculations. Mixins that computed spacing, sizing, or layout values at build time are replaced by calc(), custom properties, and the native grid and flexbox specifications that Sass was never designed to anticipate.

What remains? Complex mixins with genuine conditional logic: loops, branching, programmatic output. These exist. They are real. They are also, in the overwhelming majority of codebases, not present. The 5 per cent that genuinely need Sass’s mixin engine know who they are. The other 95 per cent are maintaining a build dependency for a feature they no longer use.

The Build Step: What You Actually Pay

The cost of Sass is not the syntax. The syntax is pleasant. The cost is the build step and everything it brings with it.

node-sass vs dart-sass. The node-sass package was deprecated in 2020 after years of native compilation failures on every platform Node.js has ever been asked to run on. Its replacement, dart-sass, is written in Dart, compiled to a standalone snapshot, and distributed through npm. The dependency chain is smaller but the migration history is a graveyard of broken CI pipelines, incompatible APIs, and deprecation warnings that arrived eighteen months after the code they deprecated.

The watcher. Every Sass project runs a file watcher in development, a process that monitors .scss files, recompiles on change, and introduces a delay between saving and seeing. Native CSS has no delay. Save the file. Reload the browser. The stylesheet you wrote is the stylesheet the browser renders. The feedback loop is instant because there is nothing in between.

The dependency. Sass is an npm package. It lives in node_modules. It participates in the supply chain. It is a dependency on a dependency ecosystem that has demonstrated, repeatedly, that trusting it requires optimism that borders on clinical.

Native CSS has no build step. No watcher. No dependency. No compilation target. No source maps to debug a transformation that should not have happened. The browser reads the file you wrote. This is not a limitation. It is the entire point.

The Bridge

Sass was a bridge. It spanned the gap between what CSS was and what CSS needed to be. It did this brilliantly, for fourteen years, and the modern CSS specification owes it a direct debt. Nesting syntax was copied from Sass. Custom properties were inspired by Sass variables. The cascade layers specification addressed the organisational problem that Sass partials solved imperfectly.

But a bridge exists to be crossed, not to be inhabited. The far bank has arrived. CSS has variables that outlive compilation. It has nesting with identical syntax. It has colour functions that are perceptually correct. It has cascade layers that impose structural order without file concatenation. It has mathematical functions that respond to reality rather than approximating it at build time.

The bridge was necessary. The bridge was excellent. The bridge is no longer the destination.

No node-sass. No dart-sass. No version conflicts. No build step. No watcher. No dependency. Browser-native. The specification caught up. The tooling can step aside.