When Sass Collides With Modern CSS

CSS custom properties and the newer CSS math functions bring real power to the language. But when you pair them with a preprocessor such as Sass, some unexpected conflicts surface. Several native CSS functions share names with Sass’s own built-in functions, and Sass assumes you want its version — which often leads to errors when you actually want the CSS one.

Where Sass Gets In the Way

The most common collision is with min() and max(). If you try min(20em, 50vh), Sass throws an error like "Incompatible units: vh and em."

Screenshot. Shows the `Incompatible units: 'em' and 'vh'` error when trying to set `width: min(20em, 50vh)`.
An error when working with different types of units in the min()/ max() function

The root cause is that Sass has its own min() function, and it takes over whenever you write the lowercase name. Sass then tries to compute the result at compile time. That’s fine when the units have a fixed relationship — 1in always equals 96px, 1s always equals 1000ms, and angle units such as turn, rad, and grad all convert cleanly to deg. But when the values use units like em and vh, which have no fixed conversion rate, Sass can’t evaluate the expression and errors out.

The same problem appears when you nest calc() inside min(). Something like min(calc(20em + 7px), 50vh) fails with a message saying calc(20em + 7px) is “not a number for min.”

Screenshot. Shows the `'calc(20em + 7px)' is not a number for 'min'` error when trying to set `width: min(calc(20em + 7px), 50vh)`.
An error when using different unit values with calc() nested in the min()function

Similar conflicts occur in CSS filter functions. Sass defines its own invert(), grayscale(), and opacity() functions, all of which expect a $color argument. Pass a CSS variable or a calc() expression to any of them, and Sass complains:

Screenshot. Shows the `$color: 'var(--p, 0.85)' is not a color for 'invert'` error when trying to set `filter: invert(var(--p, .85))`.
var() in filter: invert() error
Screenshot. Shows the `$color: 'calc(.2 + var(--d, .3))' is not a color for 'grayscale'` error when trying to set `filter: grayscale(calc(.2 + var(--d, .3)))`.
calc() in filter: grayscale() error
Screenshot. Shows the `$color: 'var(--p, 0.8)' is not a color for 'opacity'` error when trying to set `filter: opacity(var(--p, 0.8))`.
var() in filter: opacity() error

The rest of the filter functions — sepia(), blur(), drop-shadow(), brightness(), contrast(), and hue-rotate() — have no Sass counterpart, so they pass through untouched and work fine with CSS variables.

The same kind of clash happens with the color functions hsl(), hsla(), rgb(), and rgba(). Sass’s versions expect color arguments, so passing a custom property that lists two or more values throws an error:

Screenshot. Shows the `wrong number of arguments (2 for 3) for 'hsl'` error when trying to set `color: hsl(9, var(--sl, 95%, 65%))`.
var() in color: hsl() error.

Plain CSS like color: hsl(9, var(--sl, 95%, 65%)) is perfectly valid — it only breaks under Sass.

Screenshot. Shows the `$color: 'var(--rgb, 128, 64, 64)' is not a color for 'rgba'` error when trying to set `color: rgba(var(--rgb, 128, 64, 64), .7)`.
var() in color: rgba() error.

If you import Compass, the problem extends to gradients. Compass defines linear-gradient() and radial-gradient() functions but never added conic-gradient(), so CSS variables work inside the latter but fail inside the former two.

Screenshot. Shows the At least two color stops are required for a linear-gradient error when trying to set background: linear-gradient(var(--c, pink), gold).
var() in background: linear-gradient() error.

Fixing the Naming Collision

The trick is a simple quirk of parsing: Sass is case-sensitive; CSS is not. If you capitalize the function name — Min() instead of min(), Invert() instead of invert() — Sass no longer recognizes it as its own built-in. The code then passes through as native CSS and works without errors. The same approach handles HSL(), HSLA(), RGB(), RGBA(), and gradient functions.

Why Sass Is Still Worth It

CSS variables are a fantastic addition, but they aren’t a complete replacement for Sass. For one thing, custom properties carry a performance cost, and debugging long chains of nested calc() expressions in DevTools is still painful. Overusing them can quickly make things worse rather than better.

Screenshot. Shows how `calc()` expressions are presented in DevTools.
Not exactly easy to figure out what’s the result of those calc() expressions.

Sass variables remain the better choice for values that behave like constants — things that don’t change by element or state. They also avoid the repetition problem that comes with writing prefixed declarations repeatedly.

But variables were never the main reason to use Sass. Looping is a feature CSS still lacks, and Sass’s loops handle jobs like generating value lists, gradient stop lists, polygon point lists, and transform lists. Consider generating positioned items. Originally, that meant writing markup with a preprocessor and then duplicating the count in Sass:

- let n = 12;

while n--
  .item
$n: 12;
$ba: 360deg/$n;
$d: 2em;

.item {
  position: absolute;
  top: 50%; left: 50%;
  margin: -.5*$d;
  width: $d; height: $d;
  /* prettifying styles */

  @for $i from 0 to $n {
    &:nth-child(#{$i + 1}) {
      transform: rotate($i*$ba) translate(2*$d) rotate(-$i*$ba);
			
      &::before { content: '#{$i}' }
    }
  }
}

That approach forced you to keep two files in sync and produced very repetitive output:

Screenshot. Shows the generated CSS, really verbose, almost completely identical transform declaration repeated for each item.
CSS generated by the above code

Now the markup preprocessor can emit the indices as custom properties, and the transform declaration can reference them directly:

- let n = 12;

body(style=`--n: ${n}`)
  - for(let i = 0; i < n; i++)
    .item(style=`--i: ${i}`)
$d: 2em;

.item {
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -.5*$d;
  width: $d;
  height: $d;
  /* prettifying styles */
  --az: calc(var(--i)*1turn/var(--n));
  transform: rotate(var(--az)) translate(2*$d) rotate(calc(-1*var(--az)));
  counter-reset: i var(--i);
	
  &::before { content: counter(i) }
}

That dramatically reduces the amount of generated code:

Screenshot. Shows the generated CSS, much more compact, no having almost the exact same declaration set on every element separately.
CSS generated by the above code

But for something like a rainbow, where the values aren’t dynamic, Sass looping is still the right tool:

@function get-rainbow($n: 12, $sat: 90%, $lum: 65%) {
  $unit: 360/$n;
  $s-list: ();
	
  @for $i from 0 through $n {
    $s-list: $s-list, hsl($i*$unit, $sat, $lum)
  }
	
  @return $s-list
}

html { background: linear-gradient(90deg, get-rainbow()) }

Sass also fills a real gap in math support. Trigonometric and other advanced math functions are now part of the CSS spec, but no browser implements them yet. Sass itself doesn’t provide them either, but Compass does. For a lot of work — generating regular polygon clip paths, star shapes, or dithering background layers — those functions are essential:

@mixin reg-poly($n: 3) {
  $ba: 360deg/$n; // base angle
  $p: (); // point coords list, initially empty
	
  @for $i from 0 to $n {
    $ca: $i*$ba; // current angle
    $x: 50%*(1 + cos($ca)); // x coord of current point
    $y: 50%*(1 + sin($ca)); // y coord of current point
    $p: $p, $x $y // add current point coords to point coords list
  }
	
  clip-path: polygon($p) // set clip-path to list of points
}

These examples also lean on loops, conditionals, and the modulo operator, all of which are cumbersome to do in pure CSS.

Mixins also solve a real browser-compatibility problem. Styling range inputs requires addressing vendor-specific pseudo-elements, and a rule like this fails silently in every browser because each one drops the entire block when it encounters an unknown selector:

input::-webkit-slider-runnable-track, 
input::-moz-range-track, 
input::-ms-track { /* common styles */ }

The only working approach is to repeat the styles for each pseudo-element:

input::-webkit-slider-runnable-track { /* common styles */ }
input::-moz-range-track { /* common styles */ }
input::-ms-track { /* common styles */ }

A mixin keeps that repetition in the compiled output where it has to exist, but not in your source code:

@mixin track() { /* common styles */ }

input {
  &::-webkit-slider-runnable-track { @include track }
  &::-moz-range-track { @include track }
  &::-ms-track { @include track }
}

Sass and CSS variables address different problems, and they can coexist. The conflicts between the two are annoying, but they’re also easy to sidestep with a capital letter — and they hardly undermine why Sass remains useful in 2020.