Sass and clamp

CSS got some pretty nifty features recently. There’s the min() and max() functions. If you use them for, say, width you can use one rule where previously you would’ve needed to use two (a width declaration followed by either min-width or max-width). But they can also be applied to font-size! That’s very nifty—we’ve never had min-font-size or max-font-size properties.

There’s also the clamp() function. That allows you to set a minimum size, a default size, and a maximum size. Again, it can be used for lengths, like width, or for font-size.

Over on thesession.org, I’ve had some media queries in place for a while now that would increase the font-size for larger screens. It’s nothing crucial, just a nice-to-have so that on wide screens, the font is bumped up accordingly. I realised I could replace all those media queries with one clamp() statement, thanks to the vw (viewport width) unit:

font-size: clamp(1rem, 1.333vw, 1.5rem);

By default, the font-size is 1.333vw (1.333% of the viewport width), but it will never get smaller than 1rem and it will never get larger than 1.5rem.

That works, but there’s a bit of an issue with using raw vw units like that. If someone is on a wide screen and they try to adjust the font size, nothing will happen. The viewport width doesn’t change when you bump the font size up or down.

The solution is to mix in some kind of unit that does respond to the font size being bumped up or down (like, say, the rem unit). Handily, clamp() allows you to combine units, just like calc(). So I can do this:

font-size: clamp(1rem, 0.5rem + 0.666vw, 1.5rem);

The result is much the same as my previous rule, but now—thanks to the presence of that 0.5rem value—the font size responds to being adjusted by the user.

You could use a full 1rem in that default value:

font-size: clamp(1rem, 1rem + 0.333vw, 1.5rem);

…but if you do that, the minimum size (1rem) will never be reached—the default value will always be larger. So in effect it’s no different than saying:

font-size: min(1.rem + 0.333vw, 1.5rem);

I mentioned this to Chris just the other day.

Anyway, I got the result I wanted. I wanted the font size to stay at the browser default size (usually 16 pixels) until the screen was larger than around 1200 pixels. From there, the font size gets gradually bigger, until it hits one and a half times the browser default (which would be 24 pixels if the default size started at 16). I decided to apply it to the :root element (which is html) using percentages:

:root {
  font-size: clamp(100%, 50% + 0.666vw, 150%);
}

(My thinking goes like this: if we take a screen width of 1200 pixels, then 1vw would be 12 pixels: 1200 divided by 100. So for a font size of 16 pixels, that would be 1.333vw. But because I’m combining it with half of the default font size—50% of 16 pixels = 8 pixels—I need to cut the vw value in half as well: 50% of 1.333vw = 0.666vw.)

So I’ve got the CSS rule I want. I dropped it in to the top of my file and…

I got an error.

There was nothing wrong with my CSS. The problem was that I was dropping it into a Sass file (.scss).

Perhaps I am showing my age. Do people even use Sass any more? I hear that post-processors usurped Sass’s dominance (although no-one’s ever been able to explain to me why they’re different to pre-processers like Sass; they both process something you’ve written into something else). Or maybe everyone’s just writing their CSS in JS now. I hear that’s a thing.

The Session is a looooong-term project so I’m very hesitant to use any technology that won’t stand the test of time. When I added Sass into the mix, back in—I think—2012 or so, I wasn’t sure whether it was the right thing to do, from a long-term perspective. But it did offer some useful functionality so I went ahead and used it.

Now, eight years later, it was having a hard time dealing with the new clamp() function. Specifically, it didn’t like the values being calculated through the addition of multiple units. I think it was clashing with Sass’s in-built ability to add units together.

I started to ask myself whether I should still be using Sass. I looked at which features I was using…

Variables. Well, now we’ve got CSS custom properties, which are even more powerful than Sass variables because they can be updated in real time. Sass variables are like const. CSS custom properties are like let.

Mixins. These can be very useful, but now there’s a lot that you can do just in CSS with calc(). The built-in darken() and lighten() mixins are handy though when it comes to colours.

Nesting. I’ve never been a fan. I know it can make the source files look tidier but I find it can sometimes obfuscate what you’re final selectors are going to look like. So this wasn’t something I was using much any way.

Multiple files. Ah! This is the thing I would miss most. Having separate .scss files for separate interface elements is very handy!

But globbing a bunch of separate .scss files into one .css file isn’t really a Sass task. That’s what build tools are for. In fact, that’s what I was already doing with my JavaScript files; I write them as individual .js files that then get concatenated into one .js file using Grunt.

(Yes, this project uses Grunt. I told you I was showing my age. But, you know what? It works. Though seeing as I’m mostly using it for concatenation, I could probably replace it with a makefile. If I’m going to use old technology, I might as well go all the way.)

I swapped out Sass variables for CSS custom properties, mixins for calc(), and removed what little nesting I was doing. Then I stripped the Sass parts out of my Grunt file and replaced them with some concatenation and minification tasks. All of this makes no difference to the actual website, but it means I’ve got one less dependency …and I can use clamp()!

Remember a little while back when I was making a dark mode for my site? I made this observation:

Let’s just take a moment here to pause and reflect on the fact that we can now use CSS to create all sorts of effects that previously required a graphic design tool like Photoshop.

It feels like something similar has happened with tools like Sass. Sass was the hare. CSS is the tortoise. Sass blazed the trail, but now native CSS can achieve much the same result.

It’s like when we used to need something like jQuery to do DOM Scripting succinctly using CSS selectors. Then we got things like querySelector() in JavaScript so we no longer needed the trailblazer.

I’ve said it before and I’ll say it again, the goal of any good library should be to get so successful as to make itself redundant. That is, the ideas and functionality provided by the tool are so useful and widely adopted that the native technologies—HTML, CSS, and JavaScript—take their cue from those tools.

You could argue that this is what happened with Flash. It certainly happened with jQuery and Sass. I’m pretty sure we’ll see the same cycle play out with frameworks like React.

Have you published a response to this? :

Responses

Mark Boulton

This kind of stuff used to take ages of tweaking, optimisation, browser testing, network and device testing. Thanks to @googlefonts, and @adactio and @csswizardry’s writing, it’s allowed to me to bypass so much QA knowing this will Just Work. Thanks all!

overflow:hidden

Just be aware that: * The clamp function doesn’t really work in Safari * Combining vw and a static unit won’t work well in Safari either — the font loads in the right calculated size, but won’t resize on resize (or change of screen orientation).

Mark Boulton

Hmmm. Any way to fix this? or is it just that Safari is lagging?

Web Granny

There’s the clamp() function. That allows you to set a minimum size, a default size, and a maximum size. Again, it can be used for lengths, like width, or for font-size. | Sass and clamp adactio.com/journal/16887

# Posted by Web Granny on Friday, May 22nd, 2020 at 10:24am

blog.jim-nielsen.com

tldr; define your colors with individual hsl values using CSS variables, then compose your color declarations using the individual hsl values while using calc where you want to do the Sass equivalent of saturate, desaturate, lighten, darken, or adjust-hue.

:root { --color-primary-h: 30; --color-primary-s: 100%; --color-primary-l: 50%;
} /* desaturate the primary color */
.element { background-color: hsl( var(--color-primary-h), calc(var(--color-primary-s) - 20%), var(--color-primary-l) );
}

Verbose, but cool! Read on for a more detailed explanation.

Prior Art

I was recently reading Jeremy’s post Sass and Clamp where he talks about moving off Sass because most of the features he needs from Sass, like variables and mixins, are available in some form or fashion in modern CSS.

Mixins. These can be very useful, but now there’s a lot that you can do just in CSS with calc(). The built-in darken() and lighten() mixins are handy though when it comes to colours.

I’ve gone through a similar journey myself of moving off Sass in order to have one less dependency between me and the browser. I’ve felt quite happy as of late with no Sass in any of my personal projects. That said, I have always missed the color functions in Sass. I always loved those. Heck, I built a tool called SassMe which helps you visualize the output of Sass color functions in real time. All of this got me thinking: could you actually do an equivalent of something like Sass’ saturate() in CSS in 2020? Short answer: you can!

(Note: after reading Jeremy’s post mentioned above, I wrote this blog post. While proof-reading before hitting publish, Jeremy quickly followed up with a similar train of thought to what you’ll find here in his post “Programming CSS to perform Sass colour functions”—its worth also checking out.)

There’s been a lot of discussion and work in the area of color functions in CSS. Tyler illustrated the potential of some of this promising work on his site ColorMe which is like SassMe but using color functions from a CSS working draft. It sounds like the particular approach he was illustrating has been abandoned but there’s renewed effort in other areas to bring color functions to CSS natively.

In her article, A User’s Guide to CSS Variables, Lea Verou outlines how you can use CSS variables to generate different shades of color in your stylesheets.

Out of the color syntaxes currently available to CSS, hsl() tends to work better for creating color variations (until we get lch(), which is far superior due to its wider range and perceptual uniformity). If we anticipate needing only lighter/darker and alpha variants, we can use a variable for both hue and saturation

She then gives the following example:

:root { --base-color-hs: 335, 100%; --base-color: hsl(var(--base-color-hs), 50%); --base-color-light: hsl(var(--base-color-hs), 85%); --base-color-dark: hsl(var(--base-color-hs), 20%); --base-color-translucent: hsla(var(--base-color-hs), 50%, .5);
}

She then explains:

We can use these variables throughout our CSS or create new variations on the fly. Yes, there’s still a little duplication—the base color lightness—but if we plan to create many alpha variations, we could create a variable with all three coordinates, or one with the lightness.

As you can see from the above, what I’m presenting here isn’t necessarily new. I think it’s just one more step beyond what Lea proposed above.

Some Background on HSL and Sass

HSL is pretty cool. It has a few problems and, as Lea alluded to, there are better things coming (lch); nonetheless, I think HSL is a great mental model for thinking about color and programmatic control of color. Chris sums it up really well:

Hue isn’t intuitive, but it’s not that weird. You take a trip around the color wheel from 0 to 360. Saturation is more obvious where 0% has all the color sucked out, like grayscale, and 100% is fully rich color at that hue. Lightness is “normal” at 50% and adds white or black as you go toward 100% and 0%, respectively. I’m sure that’s not the correct scientific or technical way of explaining it, but that’s the brain logic.

So HSL is great for manipulating color. I actually learned just how great when I was building SassMe. Want to know the secret to how it works? Under the hood, it essentially takes a HEX color, converts it to HSL, maps one of the Sass color functions to the color value by adding/subtracting the appropriate h, s, or l values, then converts it back to a color for the browser (I built this before HSL existed as a viable option for declaring a color in the browser).

Sass has the following functions, each of which essentially takes a color, puts it in the HSL color space, then adds/subtracts the value as defined by the developer.

  • adjust-hue which adds/subtracts from the h value
  • saturate which adds to the s value
  • desaturate which subtracts from the s value
  • lighten which adds to the l value
  • darken which subtracts from the l value

So if you looked at an implementation of these functions, conceptually you’d see something something like this:

.element { background-color: lighten(#0000ff, 5%);
} /* What is being done in the above? It’s basically: Convert #0000ff to hsl equivalent — hsl(240, 100%, 50%) Adds 5% to the l value — hsl(240, 100%, 55%) Convert it back to hex color — #1a1aff
*/

Maybe you already see where this is going: those particular color functions are merely adding/subtracting values from hsl color values, and we have a way to add/subtract values in CSS with calc()!

Doing Color Functions in CSS

So if you wanted to do the equivalent of Sass’ hsl color functions in CSS, how would you do it?

First, pick a color and define its component hsl parts as independent values using CSS variables. In a real code base you’re likely to have more than one color so you’d want to give your variables good names to tell them apart, like --color-primary-h, but for simplicity’s sake in my example I’m going to just call it --h. Once you have the component hsl color values defined, you can compose them together in an hsl() function in CSS.

:root { --h: 100; --s: 50%; --l: 50%;
} .hsl-element { background-color: hsl(var(--h), var(--s), var(--l));
}

You could drop those in an hsla() too and be able to control the alpha channel on a case-by-case basis if you wanted.

.hsla-element { background-color: hsla(var(--h), var(--s), var(--l), .5);
}

“Ok,” you might say, ”that’s neat and all, but if I just want my base color, having to write out the nested variables inside an hsl() function every time can get tiring.” That’s true. So make an --hsl variable out of your base h, s, and l variables (which you could also mix with hsla()).

:root { --h: 100; --s: 50%; --l: 50%; --hsl: var(--h), var(--s), var(--l);
} .hsl-element { background-color: hsl(var(--hsl));
} .hsla-element { background-color: hsla(var(--hsl), .5);
}

You could take that a step further if you really wanted to and just cut out having to type hsl every time by defining the hsl function in a variable value. Here’s the entirety of these composable pieces:

:root { --h: 100; --s: 50%; --l: 50%; --hsl: var(--h), var(--s), var(--l); --hslf: hsl(var(--hsl));
} .hsl-element { background-color: hsl(var(--h), var(--s), var(--l));
} .hsla-element { background-color: hsla(var(--hsl), .5);
} .hsl-function-element { background-color: var(--hslf);
}

Now you have all the ingredients you need to mix-n-match how you want to declare your colors. This enables you to start using calc() to modify HSL values for your color palette on the fly.

:root { --h: 100; --s: 50%; --l: 50%; --hsl: var(--h), var(--s), var(--l); --hslf: hsl(var(--hsl));
} .normal { background-color: var(--hslf);
} .adjust-hue { background-color: hsl( calc(var(--h) + 100), var(--s), var(--l) );
} .saturate { background-color: hsl( var(--h), calc(var(--s) + 20%), var(--l) );
} .desaturate { background-color: hsl( var(--h), calc(var(--s) - 20%), var(--l) );
} .lighten { background-color: hsl( var(--h), var(--s), calc(var(--l) + 20%) );
} .darken{ background-color: hsl( var(--h), var(--s), calc(var(--l) - 20%) );
}

You can check this all out in action on my codepen and see all the different ways you can play with color in this fashion, from alpha channels:

To Sass color functions, like adjust-hue():

saturate() and desaturate()

lighten() and darken()

What’s really neat about this is that the browser seems to handle min/max on your color calculations for you. For example, if you have a color value like hsl(50, 100%, 50%) and you add 700% to the l value resulting in a value like hsl(50, 100%, 750%), that value gets interpreted by the browser at the l’s max value of 100% (i.e. hsl(50, 100%, 100%)). This is true for the h, s, or l values. This helps you not break color appearances because, for example, you saturated the color too much. It also absolves you from having to leverage min/max in CSS and writing something like min(calc(var(--h) + 100), 360).

Caveats

Obviously the problem with this is naming. I’ve tricked you with using short names like --h, --s, and --l. More likely you’re going to have a palette of named colors and each one would have to have these variants. Think functional composition in CSS.

:root { /* primary color / --color-primary-h: 50; --color-primary-s: 50%; --color-primary-l: 50%; --color-primary-hsl: var(--color-primary-h), var(--color-primary-s), var(--color-primary-l); --color-primary-hslf: hsl(var(--color-primary-hsl)); / accent color / --color-accent-h: 38; --color-accent-s: 75%; --color-accent-l: 35%; --color-accent-hsl: var(--color-accent-h), var(--color-accent-s), var(--color-accent-l); --color-accent-hslf: hsl(var(--color-accent-hsl)); / all my other colors here... */
}

That’s a lot of writing. Because of the declarative nature of CSS, you’re never going to get something as terse as what you could get in Sass. So sure, you’re typing more characters. But you know what you’re not doing? Wrangling build plugins and updating dependencies to get Sass to build. What you write gets shipped directly to the browser and works as-is, now and for eternity. It’s hard to say that about your Sass code.

# Saturday, May 30th, 2020 at 7:00pm

Veerle Pieters

Sass and Clamp @adactio writes about using the CSS clamp() function. “…the clamp() function. That allows you to set a minimum size, a default size, and a maximum size. Again, it can be used for lengths, like width, or for font-size…” adactio.com/journal/16887

2 Likes

# Liked by George Salib® on Wednesday, May 13th, 2020 at 4:02pm

# Liked by Tomáš Jakl on Wednesday, May 13th, 2020 at 8:51pm

Related posts

Hanging punctuation in CSS

A little fix for Safari.

Speedier tunes

Improving performance with containment.

Assumption

Separate your concerns.

Let’s get logical

Let me hear your blocky talk.

Control

Trying to understand a different mindset to mine.

Related links

Printing music with CSS grid

Laying out sheet music with CSS grid—sounds extreme until you see it abstracted into a web component.

We need fluid and responsive music rendering for the web!

Tagged with

Can you feel the rhythm‽ · 13 March 2024

Adam makes a very good point here: the term “vertical rhythm” is quite chauvanistic, unconciously defaulting to top-to-bottom writing modes; the term “logical rhythm” is more universal (and scalable).

Tagged with

Playing with Infinity in CSS / Coder’s Block

CSS has an infinity constant.

I did not know this.

Tagged with

[ We Need to Talk About the Front Web / Part 1: Introduction : Ge Ricci ]

Angela has turned her talk from FFconf into a five-part series of articles. I’m biased because I already agree with everything in here, but it’s well worth reading.

Tagged with

WebKit Features in Safari 17.2 | WebKit

Lots of new features landing in Safari, and it’s worth paying attention to the new icon requirements now that websites can be added to the dock:

To provide the best user experience on macOS, supply at least one opaque, full-bleed maskable square icon in the web app manifest, either as SVG (any size) or high resolution bitmap (1024×1024).

Tagged with

Previously on this day

6 years ago I wrote Unworn Pleasures

The T-shirt of the band of the scientist.

9 years ago I wrote 100 words 052

Day fifty two.

9 years ago I wrote Small independent pieces, loosely joined

Lessons learned from Indie Web Camp Germany.

11 years ago I wrote Cheap’n’cheerful

The street will find its own uses for this device.

11 years ago I wrote dConstruct bulletin

Some announcements about dConstruct 2013.

17 years ago I wrote Redesigns a go-go

Suddenly everybody is spring cleaning their websites.

18 years ago I wrote The Empire Moves House

Moving house, one lego spaceship at a time.

19 years ago I wrote Missing the summit

I’ve had an absolutely great time in Alaska…

21 years ago I wrote Virtual Festival

Congratulations to Jessica and Richard, both of whom have been shortlisted in the "Best Personal Site and Weblogs" category of the Brighton & Hove Virtual Festival Awards.