Recently, CSS has added a lot of new cool features such as custom properties and new functions. While these things can make our lives a lot easier, they can also end up interacting with preprocessors, like Sass, in funny ways.
So this is going to be a post about the issues I’ve encountered, how I go around them, and why I still find Sass necessary these days.
The errors
If you’ve played with the new min()
and max()
functions, you may have ran into an error message like this when working with different units: “Incompatible units: vh
and em
.”
This is because Sass has its ownmin()
function, and ignores the CSS min()
function. Plus, Sass cannot perform any sort of computation using two values with units that don’t have a fixed relation between them.
For example, cm
and in
units have a fixed relation between them, so Sass can figure out what’s the result of min(20in, 50cm)
and doesn’t throw an error when we try to use it in our code.
The same things goes for other units. Angular units, for example, all have a fixed relation between them: 1turn
, 1rad
or 1grad
always compute to the same deg
values. Same goes for 1s
which is always 1000ms
, 1kHz
which is always 1000Hz
, 1dppx
which is always 96dpi
, and 1in
which is always 96px
. This is why Sass can convert between them and mix them in computations and inside functions such as its own min()
function.
But things break when these units don’t have a fixed relation between them (like the earlier case with em
and vh
units).
And it’s not just different units. Trying to use calc()
inside min()
also results in an error. If I try something like calc(20em + 7px)
, the error I get is, “calc(20em + 7px)
is not a number for min
.”
Another problem arises when we want to use a CSS variable or the result of a mathematical CSS function (such as calc()
, min()
or max()
) in a CSS filter like invert()
.
In this case, we get told that “$color: 'var(--p, 0.85)
is not a color for invert
.”
The same thing happens for grayscale()
: “$color
: ‘calc(.2 + var(--d, .3))
‘ is not a color for grayscale
.”
opacity()
causes the same issue: “$color
: ‘var(--p, 0.8)
‘ is not a color for opacity
.”
However, other filter
functions — including sepia()
, blur()
, drop-shadow()
, brightness()
, contrast()
and hue-rotate()
— all work just fine with CSS variables!
Turns out that what’s happening is similar to the min()
and max()
problem. Sass doesn’t have built-in sepia()
, blur()
, drop-shadow()
, brightness()
, contrast()
, hue-rotate()
functions, but it does have its own grayscale()
, invert()
and opacity()
functions, and their first argument is a $color
value. Since it doesn’t find that argument, it throws an error.
For the same reason, we also run into trouble when trying to use a CSS variable that lists at least two hsl()
or hsla()
values.
On the flip side, color: hsl(9, var(--sl, 95%, 65%))
is perfectly valid CSS and works just fine without Sass.
The exact same thing happens with the rgb()
and rgba()
functions.
Furthermore, if we import Compass and try to use a CSS variable inside a linear-gradient()
or inside a radial-gradient()
, we get another error, even though using variables inside conic-gradient()
works just fine (that is, if the browser supports it).
This is because Compass comes with linear-gradient()
and radial-gradient()
functions, but has never added a conic-gradient()
one.
The problems in all of these cases arise from Sass or Compass having identically-named functions and assuming those are what we intended to use in our code.
Drat!
The solution
The trick here is to remember that Sass is case-sensitive, but CSS isn’t.
That means we can write Min(20em, 50vh)
and Sass won’t recognize it as its own min()
function. No errors will be thrown and it’s still valid CSS that works as intended. Similarly, writing HSL()
/ HSLA()
/ RGB()
/ RGBA()
or Invert()
allows us to avoid issues we looked at earlier.
As for gradients, I usually prefer linear-Gradient()
and radial-Gradient()
just because it’s closer to the SVG version, but using at least one capital letter in there works just fine.
But why?
Almost every time I tweet anything Sass-related, I get lectured on how it shouldn’t be used now that we have CSS variables. I thought I’d address that and explain why I disagree.
First, while I find CSS variables immensely useful and have used them for almost everything for the past three years, it’s good to keep in mind that they come with a performance cost and that tracing where something went wrong in a maze of calc()
computations can be a pain with our current DevTools. I try not to overuse them to avoid getting into a territory where the downsides of using them outweigh the benefits.
In general, if it acts like a constant, doesn’t change element-to-element or state-to-state (in which case custom properties are definitely the way to go) or reduce the amount of compiled CSS (solving the repetition problem created by prefixes), then I’m going to use a Sass variable.
Secondly, variables have always been a pretty small portion of why I use Sass. When I started using Sass in late 2012, it was primarily for looping, a feature we still don’t have in CSS. While I’ve moved some of that looping to an HTML preprocessor (because it reduces the generated code and avoids having to modify both the HTML and the CSS later), I still use Sass loops in plenty of cases, like generating lists of values, stop lists inside gradient functions, lists of points inside a polygon function, lists of transforms, and so on.
Here’s an example. I used to generate n
HTML items with a preprocessor. The choice of preprocessor matters less, but I’ll be using Pug here.
- let n = 12;
while n--
.item
Then I would set the $n
variable into the Sass (and it would have to be equal to that in the HTML) and loop up to it to generate the transforms that would position each 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}' }
}
}
}
However, this meant that I would have to change both the Pug and the Sass when changing the number of items, making the generated code very repetitive.
I have since moved to making Pug generate the indices as custom properties and then use those in the transform
declaration.
- 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) }
}
This significantly reduces the generated code.
However, looping in Sass is still necessary if I want to generate something like a rainbow.
@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()) }
Sure, I could generate it as a list variable from Pug, but doing so doesn’t take advantage of the dynamic nature of CSS variables and it doesn’t reduce the amount of code that gets served to the browser, so there’s no benefit coming out of it.
Another big part of my Sass (and Compass) use is tied to built-in mathematical functions (such as trigonometric functions), which are part of the CSS spec now, but not yet implemented in any browser. Sass doesn’t come with these functions either, but Compass does and this is why I often need to use Compass.
And, sure, I could write my own such functions in Sass. I did resort to this in the beginning, before Compass supported inverse trigonometric functions. I really needed them, so I wrote my own based on the Taylor series. But Compass provides these sorts of functions nowadays and they are better and more performant than mine.
Mathematical functions are extremely important for me as I’m a technician, not an artist. The values in my CSS usually result from mathematical computations. They’re not magic numbers or something used purely for aesthetics. A example is generating lists of clip paths points that create regular or quasi-regular polygons. Think about the case where we want to create things like non-rectangular avatars or stickers.
Let’s consider a regular polygon with vertices on a circle with a radius 50%
of the square element we start from. Dragging the slider in the following demo allows us to see where the points are placed for different numbers of vertices:
Putting it into Sass code, we have:
@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
}
Note that here we’re also making use of looping and of things such as conditionals and modulo that are a real pain when using CSS without Sass.
A slightly more evolved version of this might involve rotating the polygon by adding the same offset angle ($oa
) to the angle of each vertex. This can be seen in the following demo. This example tosses in a star mixin that works in a similar manner, except we always have an even number of vertices and every odd-indexed vertex is situated on a circle of a smaller radius ($f*50%
, where $f
is sub-unitary):
We can also have chubby stars like this:
Or stickers with interesting border
patterns. In this particular demo, each sticker is created with a single HTML element and the border
pattern is created with clip-path
, looping and mathematics in Sass. Quite a bit of it, in fact.
Another example are these card backgrounds where looping, the modulo operation and exponential functions work together to generate the dithering pixel background layers:
This demo just happens to rely heavily on CSS variables as well.
Then there’s using mixins to avoid writing the exact same declarations over and over when styling things like range inputs. Different browsers use different pseudo-elements to style the components of such a control, so for every component, we have to set the styles that control its look on multiple pseudos.
Sadly, as tempting as it may be to put this in our CSS:
input::-webkit-slider-runnable-track,
input::-moz-range-track,
input::-ms-track { /* common styles */ }
…we cannot do it because it doesn’t work! The entire rule set is dropped if even one of the selectors isn’t recognized. And since no browser recognises all three of the above, the styles don’t get applied in any browser.
We need to have something like this if we want our styles to be applied:
input::-webkit-slider-runnable-track { /* common styles */ }
input::-moz-range-track { /* common styles */ }
input::-ms-track { /* common styles */ }
But that can mean a lot of identical styles repeated three times. And if we want to change, say, the background
of the track, we need to change it in the ::-webkit-slider-runnable-track
styles, in the ::-moz-range-track
styles and in the ::-ms-track
styles.
The only sane solution we have is to use a mixin. The styles get repeated in the compiled code because they have to be repeated there, but we don’t have to write the same thing three times anymore.
@mixin track() { /* common styles */ }
input {
&::-webkit-slider-runnable-track { @include track }
&::-moz-range-track { @include track }
&::-ms-track { @include track }
}
The bottom line is: yes, Sass is still very much necessary in 2020.
Beautiful article. Makes so many things clear. I figured out that the css and sass variables not intermingle well.The point about case sensitivity really blew my mind. Thanks!! You are a gifted css programmer
Hi Ana,
You can avoid several of these issues by using the latest version of Dart Sass. I highly recommend switching off Node/LibSass at this point. We’ve been making big improvements to Sass over the last year, but the LibSass & NodeSass projects are lagging behind on those updates.
That will solve all your color-function issues, as well as your
min()
&max()
examples. There are still some remaining conflicts, like you mentioned above. We’re working to address them as quickly as possible. If you are on Dart Sass (the official core implementation of the language), you will get those updates much faster.Dart Sass also has a powerful module system that will help us avoid these issues moving forward.
And for the record, getting on the latest DartSass is an active project at CodePen! It’s not live yet, but it shouldn’t be too much longer.
In Dart Sass 1.26.9, I still get the min(), max(), and clamp() “Incompatible units” compilation failures for things like Ana mentioned…
font-size: clamp(1em, .5rem + 1vw, 2em);
width: min(960px, 90%);
The way I’ve worked around it is by enclosing the values in a calc() function…
font-size: calc(clamp(1em, .5rem + 1vw, 2em));
width: calc(min(960px, 90%));
For anyone else who’s having such issues, this does the trick now, is valid vanilla CSS, and (I would hope) will still work just fine when Sass is updated to handle these newer CSS functions–if it hasn’t been already by the time you read this!
Hey Greg!
This is an issue I ran into. It’s because of the calculation inside the value of the declaration. The good things is this is being tracked to fix:
https://github.com/sass/sass/issues/2860
I’ve had some success in putting the the value of the declaration in a CSS Custom Property and calling it that way.
That’s a good idea, too, Stuart! I’ll stick with the calc() function for now, but I like that.
Glad the fix is coming for clamp().
You can also escape any conflicting functions by escaping them, eg
#{'min(50%, 3rem)'}
, it’s what I’ve been doing lately to use min().Yeah, I was just thinking that… Isn’t escaping slightly more cascade like in that you let the next appropriate program (in this case the browser’s CSS interpreter) solve the problem? If I see Min() and min() in code, before reading this article, it wouldn’t communicate to me what’s happening. Escaping the the SASS so CSS can do the job, and will communicate to me what’s happening. Using case to solve the problem will work if you’re the only person coding, but using case is a no-go for teams!
Changing function case creates a bit of noise to my eyes, but it’s an interesting and easy workaround.
I’ve make another choice: overwriting the SASS
hsl
function with a custom one (available as a package).An alternative to hacking around the ambiguity between min() etc with casing or using modules in newer SASS would be for SASS to treat such functions as optimization opportunities if it can evaluate them and otherwise leave them in place. In my opinion, that would be an actual reason to use SASS. Otherwise, using JavaScript with an appropriate template tag might just be cleaner and simpler while also removing yet another tool from the plethora of build time dependencies. Mind you JavaScript comes with all the trigonometric goodness you desire anyways. In short, SASS has a real opportunity for adding value here that isn’t easily realized otherwise.
I realize that this may only impact a small portion of readers, but I also have to say that as lovely as CSS variables are… they’re not compatible with IE11 at all. Who cares about IE11? Sadly, some of my clients (depending on industry).
And that’s where the convenience of SASS comes to the rescue; we can do a lot and still remain compatible with crappier, unmaintained, older browsers. If I abandoned it, I could see being backward compatible being that much more time consuming.
I have to support IE11, and am using CSS variables.
Get this ponyfill – https://github.com/jhildenbiddle/css-vars-ponyfill
And also ensure that you declare CSS vars in the
:root
element.In reference to the final example, it’s better and more reusable to use
@content
in the mixin for assigning the styles to the prefixes.Though if you use Autoprefixer, adding prefixes through Sass shouldn’t be necessary.
Some great stuff here but it kinda feels like 2 different articles. One about dealing with new CSS functions that have the same name as functions in Sass and another about why Sass is still useful in modern web development
Thanks for such detailed write-up, I definitely ran into the min max problem and couldn’t figure it out.
As for your last piece, we have a different mixin for handling that:
We use it like this:
Division is also impossible it seems:
https://jsbin.com/zaduhiq/1/edit?html,css,js,output