Sass – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Wed, 28 Sep 2022 14:30:24 +0000 en-US hourly 1 https://wordpress.org/?v=6.5.5 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 Sass – CSS-Tricks https://css-tricks.com 32 32 45537868 How I Made an Icon System Out of CSS Custom Properties https://css-tricks.com/how-i-made-an-icon-system-out-of-css-custom-properties/ https://css-tricks.com/how-i-made-an-icon-system-out-of-css-custom-properties/#comments Thu, 22 Sep 2022 15:17:21 +0000 https://css-tricks.com/?p=373111 SVG is the best format for icons on a website, there is no doubt about that. It allows you to have sharp icons no matter the screen pixel density, you can change the styles of the SVG on hover …


How I Made an Icon System Out of CSS Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
SVG is the best format for icons on a website, there is no doubt about that. It allows you to have sharp icons no matter the screen pixel density, you can change the styles of the SVG on hover and you can even animate the icons with CSS or JavaScript.

There are many ways to include an SVG on a page and each technique has its own advantages and disadvantages. For the last couple of years, I have been using a Sass function to import directly my icons in my CSS and avoid having to mess up my HTML markup.

I have a Sass list with all the source codes of my icons. Each icon is then encoded into a data URI with a Sass function and stored in a custom property on the root of the page.

TL;DR

What I have for you here is a Sass function that creates a SVG icon library directly in your CSS.

The SVG source code is compiled with the Sass function that encodes them in data URI and then stores the icons in CSS custom properties. You can then use any icon anywhere in your CSS like as if it was an external image.

This is an example pulled straight from the code of my personal site:

.c-filters__summary h2:after {
  content: var(--svg-down-arrow);
  position: relative;
  top: 2px;
  margin-left: auto;
  animation: closeSummary .25s ease-out;
}

Demo

Sass structure

/* All the icons source codes */
$svg-icons: (
  burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0...'
);

/* Sass function to encode the icons */
@function svg($name) {
  @return url('data:image/svg+xml, #{$encodedSVG} ');
}

/* Store each icon into a custom property */
:root {
  @each $name, $code in $svg-icons {
    --svg-#{$name}: #{svg($name)};
  }
}

/* Append a burger icon in my button */
.menu::after {
  content: var(--svg-burger);
}		

This technique has both pros and cons, so please take them into account before implementing this solution on your project:

Pros

  • There are no HTTP requests for the SVG files.
  • All of the icons are stored in one place.
  • If you need to update an icon, you don’t have to go over each HTML templates file.
  • The icons are cached along with your CSS.
  • You can manually edit the source code of the icons.
  • It does not pollute your HTML by adding extra markup.
  • You can still change the color or some aspect of the icon with CSS.

Cons

  • You cannot animate or update a specific part of the SVG with CSS.
  • The more icons you have, the heavier your CSS compiled file will be.

I mostly use this technique for icons rather than logos or illustrations. An encoded SVG is always going to be heavier than its original file, so I still load my complex SVG with an external file either with an <img> tag or in my CSS with url(path/to/file.svg).

Encoding SVG into data URI

Encoding your SVG as data URIs is not new. In fact Chris Coyier wrote a post about it over 10 years ago to explain how to use this technique and why you should (or should not) use it.

There are two ways to use an SVG in your CSS with data URI:

  • As an external image (using background-image,border-image,list-style-image,…)
  • As the content of a pseudo element (e.g. ::before or ::after)

Here is a basic example showing how you how to use those two methods:

The main issue with this particular implementation is that you have to convert the SVG manually every time you need a new icon and it is not really pleasant to have this long string of unreadable code in your CSS.

This is where Sass comes to the rescue!

Using a Sass function

By using Sass, we can make our life simpler by copying the source code of our SVG directly in our codebase, letting Sass encode them properly to avoid any browser error.

This solution is mostly inspired by an existing function developed by Threespot Media and available in their repository.

Here are the four steps of this technique:

  • Create a variable with all your SVG icons listed.
  • List all the characters that needs to be skipped for a data URI.
  • Implement a function to encode the SVGs to a data URI format.
  • Use your function in your code.

1. Icons list

/**
* Add all the icons of your project in this Sass list
*/
$svg-icons: (
  burger: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24.8 18.92" width="24.8" height="18.92"><path d="M23.8,9.46H1m22.8,8.46H1M23.8,1H1" fill="none" stroke="#000" stroke-linecap="round" stroke-width="2"/></svg>'
);

2. List of escaped characters

/**
* Characters to escape from SVGs
* This list allows you to have inline CSS in your SVG code as well
*/
$fs-escape-chars: (
  ' ': '%20',
  '\'': '%22',
  '"': '%27',
  '#': '%23',
  '/': '%2F',
  ':': '%3A',
  '(': '%28',
  ')': '%29',
  '%': '%25',
  '<': '%3C',
  '>': '%3E',
  '\\': '%5C',
  '^': '%5E',
  '{': '%7B',
  '|': '%7C',
  '}': '%7D',
);

3. Encode function

/**
* You can call this function by using `svg(nameOfTheSVG)`
*/
@function svg($name) {
  // Check if icon exists
  @if not map-has-key($svg-icons, $name) {
    @error 'icon “#{$name}” does not exists in $svg-icons map';
    @return false;
  }

  // Get icon data
  $icon-map: map-get($svg-icons, $name);

  $escaped-string: '';
  $unquote-icon: unquote($icon-map);
  // Loop through each character in string
  @for $i from 1 through str-length($unquote-icon) {
    $char: str-slice($unquote-icon, $i, $i);

    // Check if character is in symbol map
    $char-lookup: map-get($fs-escape-chars, $char);

    // If it is, use escaped version
    @if $char-lookup != null {
        $char: $char-lookup;
    }

    // Append character to escaped string
    $escaped-string: $escaped-string + $char;
  }

  // Return inline SVG data
  @return url('data:image/svg+xml, #{$escaped-string} ');
}		

4. Add an SVG in your page

button {
  &::after {
    /* Import inline SVG */
    content: svg(burger);
  }
}

If you have followed those steps, Sass should compile your code properly and output the following:

button::after {
  content: url("data:image/svg+xml, %3Csvg%20xmlns=%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox=%270%200%2024.8%2018.92%27%20width=%2724.8%27%20height=%2718.92%27%3E%3Cpath%20d=%27M23.8,9.46H1m22.8,8.46H1M23.8,1H1%27%20fill=%27none%27%20stroke=%27%23000%27%20stroke-linecap=%27round%27%20stroke-width=%272%27%2F%3E%3C%2Fsvg%3E ");
}		

Custom properties

The now-implemented Sass svg() function works great. But its biggest flaw is that an icon that is needed in multiple places in your code will be duplicated and could increase your compiled CSS file weight by a lot!

To avoid this, we can store all our icons into CSS variables and use a reference to the variable instead of outputting the encoded URI every time.

We will keep the same code we had before, but this time we will first output all the icons from the Sass list into the root of our webpage:

/**
  * Convert all icons into custom properties
  * They will be available to any HTML tag since they are attached to the :root
  */

:root {
  @each $name, $code in $svg-icons {
    --svg-#{$name}: #{svg($name)};
  }
}

Now, instead of calling the svg() function every time we need an icon, we have to use the variable that was created with the --svg prefix.

button::after {
  /* Import inline SVG */
  content: var(--svg-burger);
}

Optimizing your SVGs

This technique does not provide any optimization on the source code of the SVG you are using. Make sure that you don’t leave unnecessary code; otherwise they will be encoded as well and will increase your CSS file size.

You can check this great list of tools and information on how to optimize properly your SVG. My favorite tool is Jake Archibald’s SVGOMG — simply drag your file in there and copy the outputted code.

Bonus: Updating the icon on hover

With this technique, we cannot select with CSS specific parts of the SVG. For example, there is no way to change the fill color of the icon when the user hovers the button. But there are a few tricks we can use with CSS to still be able to modify the look of our icon.

For example, if you have a black icon and you want to have it white on hover, you can use the invert() CSS filter. We can also play with the hue-rotate() filter.

Bonus #2: Updating the icon using CSS mask-image property

Another trick to be able to change the color of your icon, is to use it as a mask on your pseudo-element with a background. Set your pseudo-element as inline-block with a background-color and define a width & height for the size needed.

Once you have a rectangle with the color needed, apply those four values to only keep the shape of the SVG needed:

  • mask-image: var(--svg-burger): The reference to our icon.
  • mask-repeat: no-repeat: To prevent the mask to be duplicated.
  • mask-size: contain: To make the icon fit perfectly in the rectangle.
  • mask-position: center: To center our icon in the pseudo-element.

Don’t forget that all CSS mask properties still need to be prefixed with -webkit- for most browsers as of September 2022.

Thanks to Christopher and Mike for letting me know about this trick in the comments!

That’s it!

I hope you find this little helper function handy in your own projects. Let me know what you think of the approach — I’d be interested to know how you’d make this better or tackle it differently!


How I Made an Icon System Out of CSS Custom Properties originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-i-made-an-icon-system-out-of-css-custom-properties/feed/ 6 373111
A Practical Tip For Using Sass Default Parameters https://css-tricks.com/sass-default-parameters/ https://css-tricks.com/sass-default-parameters/#respond Thu, 13 Jan 2022 15:17:28 +0000 https://css-tricks.com/?p=360423 Sass offers functions and mixins that accept parameters. You can use Sass default parameters, that is, parameters that have a value even if you don’t provide them when the function or mixin is called.

Let’s focus on mixins here. …


A Practical Tip For Using Sass Default Parameters originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Sass offers functions and mixins that accept parameters. You can use Sass default parameters, that is, parameters that have a value even if you don’t provide them when the function or mixin is called.

Let’s focus on mixins here. Here’s the syntax of a mixin:

@mixin foo($a, $b, $c) {
  // I can use $a, $b, and $c in here, but there is a risk they are null
}

.el {
  @include foo(1, 2, 3);

  // if I tried to do `@include foo;`
  // ... which is valid syntax... 
  // I'd get `Error: Missing argument $a.` from Sass
}

It’s safer and more useful to set up default parameters in this Sass mixin:

@mixin foo($a: 1, $b: 2, $c: 3) {
}

.el {
  // Now this is fine
  @include foo;

  // AND I can send in params as well
  @include foo("three", "little", "pigs");
}

But what if I wanted to send in $b and $c, but leave $a as the Sass default parameter? The trick is that you send in named parameters:

@mixin foo($a: 1, $b: 2, $c: 3) {
}

.el {
  // Only sending in the second two params, $a will be the default.
  @include foo($b: 2, $c: 3);
}

A real-life example using Sass default parameters

Here’s a quick-y mixin that outputs what you need for very basic styled scrollbars (Kitty has one as well):

@mixin scrollbars(
  $size: 10px,
  $foreground-color: #eee,
  $background-color: #333
) {
  // For Google Chrome
  &::-webkit-scrollbar {
    width: $size;
    height: $size;
  }
  &::-webkit-scrollbar-thumb {
    background: $foreground-color;
  }
  &::-webkit-scrollbar-track {
    background: $background-color;
  }

  // Standard version (Firefox only for now)
  scrollbar-color: $foreground-color $background-color;
}

Now I can call it like this:

.scrollable {
  @include scrollbars;
}

.thick-but-otherwise-default-scrollable {
  // I can skip $b and $c because they are second and third
  @include scrollbars(30px);
}

.custom-colors-scrollable {
  // I can skip the first param if all the others are named.
  @include scrollbars($foreground-color: orange, $background-color: black);
}

.totally-custom-scrollable {
  @include scrollbars(20px, red, black);
}

I’m just noting this as I had to search around a bit to figure this out. I was trying stuff like sending empty strings or null as the first parameter in order to “skip” it, but that doesn’t work. Gotta do the named parameter approach.


A Practical Tip For Using Sass Default Parameters originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/sass-default-parameters/feed/ 0 360423
How I Made a Generator for SVG Loaders With Sass and SMIL Options https://css-tricks.com/how-i-made-a-generator-for-svg-loaders-with-sass-and-smil-options/ https://css-tricks.com/how-i-made-a-generator-for-svg-loaders-with-sass-and-smil-options/#comments Thu, 26 Aug 2021 14:35:03 +0000 https://css-tricks.com/?p=350223 While learning Vue.js, I started building free web tools that involved the exploration of SVG, with the goal of learning something about both! Let’s take a look at one of those tools: a generator that makes SVG loaders and …


How I Made a Generator for SVG Loaders With Sass and SMIL Options originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
While learning Vue.js, I started building free web tools that involved the exploration of SVG, with the goal of learning something about both! Let’s take a look at one of those tools: a generator that makes SVG loaders and lets you choose between SMIL or Sass animation, different styles, colors, shapes, and effects. It even lets you paste in a custom path or text, and then download the final SVG, copy the code, or open a demo over at CodePen.

How it started

Three coincidences led me to build a generator for SVG loaders.

Coincidence 1: Sarah Drasner’s book

The first time I read about Sass loops was in Sarah Drasner’s SVG Animations. She shows how to stagger animations with a Sass function (like the does in Chapter 6, “Animating Data Visualizations”).

I was inspired by that chapter and the possibilities of Sass loops.

Coincidence 2: A GIF

At that same point in life, I was asked to replicate a “loader” element, similar to Apple’s old classic.

A round segmented spinner where each segment fades in and out in succession to create a circling effect.
This is a mockup of the loader I was asked to make.

I referenced Sarah’s example to make it happen. This is the Sass loop code I landed on:

@for $i from 1 to 12 {
  .loader:nth-of-type(#{$i}) {
    animation: 1s $i * 0.08s opacityLoader infinite;
  }
}
@keyframes opacityLoader {
 to { opacity: 0; }
}

This defines a variable for a number (i) from 1 to 12 that increases the delay of the animation with every :nth-child element. It was the perfect use case to animate as many elements as I wanted with only two lines of Sass, saving me CSS declarations for each of the delays I needed. This is the same animation, but written in vanilla CSS to show the difference:

.loader:nth-of-type(1) {
  animation: 1s 0.08s opacityLoader infinite;
}
.loader:nth-of-type(2) {
  animation: 1s 0.16s opacityLoader infinite;
}

/* ... */

.loader:nth-of-type(12) {
  animation: 1s 0.96s opacityLoader infinite;
}
@keyframes opacityLoader {
  to { opacity: 0; }
}

Coincidence 3: An idea

With these things going on in my head, I had an idea for a gallery of loaders, where each loader is made from the same Sass loop. I always struggle to find these kinds of things online, so I thought it might be useful for others, not to mention myself.

I had already built this kind of thing before as a personal project, so I ended up building a loader generator. Let me know if you find bugs in it!

One loader, two outputs

I was blocked by my own developer skills while creating a generator that produces the right Sass output. I decided to try another animation approach with SMIL animations, and that’s what I wound up deciding to use.

But then I received some help (thanks, ekrof!) and got Sass to work after all.

So, I ended up adding both options to the generator. I found it was a challenge to make both languages return the same result. In fact, they sometimes produce different results.

SMIL vs. CSS/Sass

I learned quite a bit about SMIL and CSS/Sass animations along the way. These are a few of the key takeaways that helped me on my way to making the generator:

  • SMIL doesn’t rely on any external resources. It animates SVG via presentation attributes directly in the SVG markup. That’s something that neither CSS nor Sass can do.
  • SMIL animations are preserved when an SVG is embedded as an image or as a background image. It is possible to add a CSS <style> block directly inside the SVG, but not so much with Sass, of course. That’s why there is an option to download the actual SVG file when selecting the SMIL option in the generator.
  • SMIL animations look a bit more fluid. I couldn’t find the reason for this (if anyone has any deeper information here, please share!). I though it was related to GPU acceleration, but it seems they both use the same animation engine.
Two spinners, one left and one right. They are both red and consist of circles that fade in and out in succession as an animated GIF.
SMIL (left) and Sass (right)

You might notice a difference in the chaining of the animations between both languages:

  • I used additive="sum" in SMIL to add animations one after the other. This makes sure each new animation effect avoids overriding the previous animation.
  • That said, in CSS/Sass, the W3C points out that [when] multiple animations are attempting to modify the same property, then the animation closest to the end of the list of names wins.

That’s why the order in which animations are applied might change the Sass output.

Working with transforms

Working with transformations in the loader’s styling was a big issue. I had applied transform: rotate inline to each shape because it’s a simple way to place them next to each other in a circle and with a face pointing toward the center.

<svg>
  <!-- etc. -->
  <use class="loader" xlink:href="#loader" transform="rotate(0 50 50)" />
  <use class="loader" xlink:href="#loader" transform="rotate(30 50 50)" />
  <use class="loader" xlink:href="#loader" transform="rotate(60 50 50)" />
  <!-- etc. -->
</svg>

I could declare a type in SMIL with <animateTransform> (e.g. scale or translate) to add that specific transform to the original transformation of each shape:

<animateTransform
  attributeName="transform"
  type="translate"
  additive="sum"
  dur="1s"
  :begin="`${i * 0.08}s`"
  repeatCount="indefinite"
  from="0 0"
  to="10"
/>

But instead, transform in CSS was overriding any previous transform applied to the inline SVG. In other words, the original position reset to 0 and showed a very different result from what SMIL produced. That meant the animations wound up looking identical no matter what.

The same two red spinners as before but with different results. The SMIL version on the left seems to work as expected but the Sass one on the right doesn't animate in a circle like it should.

The (not very pretty) solution to make the Sass similar to SMIL was to place each shape inside a group (<g>) element, and apply the inline rotation to the groups, and the animation to the shapes. This way, the inline transform isn’t affected by the animation.

<svg>
  <!-- etc. -->
  <g class="loader" transform="rotate(0 50 50)">
    <use xlink:href="#loader" />
  </g>
  <g class="loader" transform="rotate(30 50 50)">
    <use xlink:href="#loader" />
  </g>
  <!-- etc. -->
</svg>

Now both languages have a very similar result.

The technology I used

I used Vue.js and Nuxt.js. Both have great documentation, but there are more specific reasons why I choose them.

I like Vue for lots of reasons:

  • Vue encapsulates HTML, CSS, and JavaScript as a “single file component” where all the code lives in a single file that’s easier to work with.
  • The way Vue binds and dynamically updates HTML or SVG attributes is very intuitive.
  • HTML and SVG don’t require any extra transformations (like making the code JSX-compatible).

As far as Nuxt goes:

  • It has a quick boilerplate that helps you focus on development instead of configuration.
  • There’s automatic routing and it supports auto-importing components.
  • It’s a good project structure with pages, components, and layouts.
  • It’s easier to optimize for SEO, thanks to meta tags.

Let’s look at a few example loaders

What I like about the end result is that the generator isn’t a one-trick pony. There’s no one way to use it. Because it outputs both SMIL and CSS/Sass, there are several ways to integrate a loader into your own project.

Download the SMIL SVG and use it as a background image in CSS

Like I mentioned earlier, SMIL features are preserved when an SVG is used as a background image file. So, simply download the SVG from the generator, upload it to your server, and reference it in CSS as a background image.

Similarly, we could use the SVG as a background image of a pseudo-element:

Drop the SVG right into the HTML markup

The SVG doesn’t have to be a background image. It’s just code, after all. That means we can simply drop the code from the generator into our own markup and let SMIL do its thing.

Use a Sass loop on the inline SVG

This is what I was originally inspired to do, but ran into some roadblocks. Instead of writing CSS declarations for each animation, we can use the Sass loop produced by the generator. The loop targets a .loader class that’s already applied to the outputted SVG. So, once Sass is compiled to CSS, we get a nice spinning animation.

I’m still working on this

My favorite part of the generator is the custom shape option where you can add text, emojis, or any SVG element to the mix:

The same circle spinner but using custom SVG shapes: one a word, one a poop emoji, and bright pink and orange asterisk.
Custom text, emoji, and SVG

What I would like to do is add a third option for styles to have just one element where you get to work with your own SVG element. That way, there’s less to work with, while allowing for simpler outputs.

The challenge with this project is working with custom values for so many things, like duration, direction, distance, and degrees. Another challenge for me personally is becoming more familiar with Vue because I want to go back and clean up that messy code. That said, the project is open source, and pull requests are welcome! Feel free to send suggestions, feedback, or even Vue course recommendations, especially ones related to SVG or making generators.

This all started with a Sass loop that I read in a book. It isn’t the cleanest code in the world, but I’m left blown away by the power of SMIL animations. I highly recommend Sarah Soueidan’s guide for a deeper dive into what SMIL is capable of doing.

If you’re curious about the safety of SMIL, that is for good reason. There was a time when Chrome was going to entirely deprecated SMIL (see the opening note in MDN). But that deprecation has been suspended and hasn’t (seemingly) been talked about in a while.

Can I use SMIL?

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
54No796

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
12612736.0-6.1

How I Made a Generator for SVG Loaders With Sass and SMIL Options originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-i-made-a-generator-for-svg-loaders-with-sass-and-smil-options/feed/ 5 350223
Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc() https://css-tricks.com/efficient-infinite-utility-helpers-using-inline-css-custom-properties-and-calc/ https://css-tricks.com/efficient-infinite-utility-helpers-using-inline-css-custom-properties-and-calc/#comments Fri, 06 Aug 2021 14:55:06 +0000 https://css-tricks.com/?p=345467 I recently wrote a very basic Sass loop that outputs several padding and margin utility classes. Nothing fancy, really, just a Sass map with 11 spacing values, looped over to create classes for both padding and margin on each side. …


Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently wrote a very basic Sass loop that outputs several padding and margin utility classes. Nothing fancy, really, just a Sass map with 11 spacing values, looped over to create classes for both padding and margin on each side. As we’ll see, this works, but it ends up a pretty hefty amount of CSS. We’re going to refactor it to use CSS custom properties and make the system much more trim.

Here’s the original Sass implementation:

$space-stops: (
  '0': 0,
  '1': 0.25rem,
  '2': 0.5rem,
  '3': 0.75rem,
  '4': 1rem,
  '5': 1.25rem,
  '6': 1.5rem,
  '7': 1.75rem,
  '8': 2rem,
  '9': 2.25rem,
  '10': 2.5rem,
);

@each $key, $val in $space-stops {
  .p-#{$key} {
    padding: #{$val} !important;
  }
  .pt-#{$key} {
    padding-top: #{$val} !important;
  }
  .pr-#{$key} {
    padding-right: #{$val} !important;
  }
  .pb-#{$key} {
    padding-bottom: #{$val} !important;
  }
  .pl-#{$key} {
    padding-left: #{$val} !important;
  }
  .px-#{$key} {
    padding-right: #{$val} !important;
    padding-left: #{$val} !important;
  }
  .py-#{$key} {
    padding-top: #{$val} !important;
    padding-bottom: #{$val} !important;
  }

  .m-#{$key} {
    margin: #{$val} !important;
  }
  .mt-#{$key} {
    margin-top: #{$val} !important;
  }
  .mr-#{$key} {
    margin-right: #{$val} !important;
  }
  .mb-#{$key} {
    margin-bottom: #{$val} !important;
  }
  .ml-#{$key} {
    margin-left: #{$val} !important;
  }
  .mx-#{$key} {
    margin-right: #{$val} !important;
    margin-left: #{$val} !important;
  }
  .my-#{$key} {
    margin-top: #{$val} !important;
    margin-bottom: #{$val} !important;
  }
}

This very much works. It outputs all the utility classes we need. But, it can also get bloated quickly. In my case, they were about 8.6kb uncompressed and under 1kb compressed. (Brotli was 542 bytes, and gzip came in at 925 bytes.)

Since they are extremely repetitive, they compress well, but I still couldn’t shake the feeling that all these classes were overkill. Plus, I hadn’t even done any small/medium/large breakpoints which are fairly typical for these kinds of helper classes.

Here’s a contrived example of what the responsive version might look like with small/medium/large classes added. We’ll re-use the $space-stops map defined previously and throw our repetitious code into a mixin

@mixin finite-spacing-utils($bp: '') {
    @each $key, $val in $space-stops {
        .p-#{$key}#{$bp} {
            padding: #{$val} !important;
        }
        .pt-#{$key}#{$bp} {
            padding-top: #{$val} !important;
        }
        .pr-#{$key}#{$bp} {
            padding-right: #{$val} !important;
        }
        .pb-#{$key}#{$bp} {
            padding-bottom: #{$val} !important;
        }
        .pl-#{$key}#{$bp} {
            padding-left: #{$val} !important;
        }
        .px-#{$key}#{$bp} {
            padding-right: #{$val} !important;
            padding-left: #{$val} !important;
        }
        .py-#{$key}#{$bp} {
            padding-top: #{$val} !important;
            padding-bottom: #{$val} !important;
        }

        .m-#{$key}#{$bp} {
            margin: #{$val} !important;
        }
        .mt-#{$key}#{$bp} {
            margin-top: #{$val} !important;
        }
        .mr-#{$key}#{$bp} {
            margin-right: #{$val} !important;
        }
        .mb-#{$key}#{$bp} {
            margin-bottom: #{$val} !important;
        }
        .ml-#{$key}#{$bp} {
            margin-left: #{$val} !important;
        }
        .mx-#{$key}#{$bp} {
            margin-right: #{$val} !important;
            margin-left: #{$val} !important;
        }
        .my-#{$key}#{$bp} {
            margin-top: #{$val} !important;
            margin-bottom: #{$val} !important;
        }
    }
}

@include finite-spacing-utils;

@media (min-width: 544px) {
    @include finite-spacing-utils($bp: '_sm');
}

@media (min-width: 768px) {
    @include finite-spacing-utils($bp: '_md');
}

@media (min-width: 1024px) {
    @include finite-spacing-utils($bp: '_lg');
}

That clocks in at about 41.7kb uncompressed (and about 1kb with Brotli, and 3kb with gzip). It still compresses well, but it’s a bit ridiculous.

I knew it was possible to reference data-* attributes from within CSS using the [attr() function, so I wondered if it was possible to use calc() and attr() together to create dynamically-calculated spacing utility helpers via data-* attributes — like data-m="1" or data-m="1@md" — then in the CSS to do something like margin: calc(attr(data-m) * 0.25rem) (assuming I’m using a spacing scale incrementing at 0.25rem intervals). That could be very powerful.

But the end of that story is: no, you (currently) can’t use attr() with any property except the content property. Bummer. But in searching for attr() and calc() information, I found this intriguing Stack Overflow comment by Simon Rigét that suggests setting a CSS variable directly within an inline style attribute. Aha!

So it’s possible to do something like <div style="--p: 4;"> then, in CSS:

:root {
  --p: 0;
}

[style*='--p:'] {
  padding: calc(0.25rem * var(--p)) !important;
}

In the case of the style="--p: 4;" example, you’d effectively end up with padding: 1rem !important;.

… and now you have an infinitely scalable spacing utility class monstrosity helper.

Here’s what that might look like in CSS:

:root {
  --p: 0;
  --pt: 0;
  --pr: 0;
  --pb: 0;
  --pl: 0;
  --px: 0;
  --py: 0;
  --m: 0;
  --mt: 0;
  --mr: 0;
  --mb: 0;
  --ml: 0;
  --mx: 0;
  --my: 0;
}

[style*='--p:'] {
  padding: calc(0.25rem * var(--p)) !important;
}
[style*='--pt:'] {
  padding-top: calc(0.25rem * var(--pt)) !important;
}
[style*='--pr:'] {
  padding-right: calc(0.25rem * var(--pr)) !important;
}
[style*='--pb:'] {
  padding-bottom: calc(0.25rem * var(--pb)) !important;
}
[style*='--pl:'] {
  padding-left: calc(0.25rem * var(--pl)) !important;
}
[style*='--px:'] {
  padding-right: calc(0.25rem * var(--px)) !important;
  padding-left: calc(0.25rem * var(--px)) !important;
}
[style*='--py:'] {
  padding-top: calc(0.25rem * var(--py)) !important;
  padding-bottom: calc(0.25rem * var(--py)) !important;
}

[style*='--m:'] {
  margin: calc(0.25rem * var(--m)) !important;
}
[style*='--mt:'] {
  margin-top: calc(0.25rem * var(--mt)) !important;
}
[style*='--mr:'] {
  margin-right: calc(0.25rem * var(--mr)) !important;
}
[style*='--mb:'] {
  margin-bottom: calc(0.25rem * var(--mb)) !important;
}
[style*='--ml:'] {
  margin-left: calc(0.25rem * var(--ml)) !important;
}
[style*='--mx:'] {
  margin-right: calc(0.25rem * var(--mx)) !important;
  margin-left: calc(0.25rem * var(--mx)) !important;
}
[style*='--my:'] {
  margin-top: calc(0.25rem * var(--my)) !important;
  margin-bottom: calc(0.25rem * var(--my)) !important;
}

This is a lot like the first Sass loop above, but there’s no loop going 11 times — and yet it’s infinite. It’s about 1.4kb uncompressed, 226 bytes with Brotli, or 284 bytes gzipped.

If you wanted to extend this for breakpoints, the unfortunate news is that you can’t put the “@” character in CSS variable names (although emojis and other UTF-8 characters are strangely permitted). So you could probably set up variable names like p_sm or sm_p. You’d have to add some extra CSS variables and some media queries to handle all this, but it won’t blow up exponentially the way traditional CSS classnames created with a Sass for-loop do.

Here’s the equivalent responsive version. We’ll use a Sass mixin again to cut down the repetition:

:root {
  --p: 0;
  --pt: 0;
  --pr: 0;
  --pb: 0;
  --pl: 0;
  --px: 0;
  --py: 0;
  --m: 0;
  --mt: 0;
  --mr: 0;
  --mb: 0;
  --ml: 0;
  --mx: 0;
  --my: 0;
}

@mixin infinite-spacing-utils($bp: '') {
    [style*='--p#{$bp}:'] {
        padding: calc(0.25rem * var(--p#{$bp})) !important;
    }
    [style*='--pt#{$bp}:'] {
        padding-top: calc(0.25rem * var(--pt#{$bp})) !important;
    }
    [style*='--pr#{$bp}:'] {
        padding-right: calc(0.25rem * var(--pr#{$bp})) !important;
    }
    [style*='--pb#{$bp}:'] {
        padding-bottom: calc(0.25rem * var(--pb#{$bp})) !important;
    }
    [style*='--pl#{$bp}:'] {
        padding-left: calc(0.25rem * var(--pl#{$bp})) !important;
    }
    [style*='--px#{$bp}:'] {
        padding-right: calc(0.25rem * var(--px#{$bp})) !important;
        padding-left: calc(0.25rem * var(--px)#{$bp}) !important;
    }
    [style*='--py#{$bp}:'] {
        padding-top: calc(0.25rem * var(--py#{$bp})) !important;
        padding-bottom: calc(0.25rem * var(--py#{$bp})) !important;
    }
    [style*='--m#{$bp}:'] {
        margin: calc(0.25rem * var(--m#{$bp})) !important;
    }
    [style*='--mt#{$bp}:'] {
        margin-top: calc(0.25rem * var(--mt#{$bp})) !important;
    }
    [style*='--mr#{$bp}:'] {
        margin-right: calc(0.25rem * var(--mr#{$bp})) !important;
    }
    [style*='--mb#{$bp}:'] {
        margin-bottom: calc(0.25rem * var(--mb#{$bp})) !important;
    }
    [style*='--ml#{$bp}:'] {
        margin-left: calc(0.25rem * var(--ml#{$bp})) !important;
    }
    [style*='--mx#{$bp}:'] {
        margin-right: calc(0.25rem * var(--mx#{$bp})) !important;
        margin-left: calc(0.25rem * var(--mx#{$bp})) !important;
    }
    [style*='--my#{$bp}:'] {
        margin-top: calc(0.25rem * var(--my#{$bp})) !important;
        margin-bottom: calc(0.25rem * var(--my#{$bp})) !important;
    }
}

@include infinite-spacing-utils;

@media (min-width: 544px) {
    @include infinite-spacing-utils($bp: '_sm');
}

@media (min-width: 768px) {
    @include infinite-spacing-utils($bp: '_md');
}

@media (min-width: 1024px) {
    @include infinite-spacing-utils($bp: '_lg');
}

That’s about 6.1kb uncompressed, 428 bytes with Brotli, and 563 with gzip.

Do I think that writing HTML like <div style="--px:2; --my:4;"> is pleasing to the eye, or good developer ergonomics… no, not particularly. But could this approach be viable in situations where you (for some reason) need extremely minimal CSS, or perhaps no external CSS file at all? Yes, I sure do.

It’s worth pointing out here that CSS variables assigned in inline styles do not leak out. They’re scoped only to the current element and don’t change the value of the variable globally. Thank goodness! The one oddity I have found so far is that DevTools (at least in Chrome, Firefox, and Safari) do not report the styles using this technique in the “Computed” styles tab.

Also worth mentioning is that I’ve used good old padding  and margin properties with -top, -right, -bottom, and -left, but you could use the equivalent logical properties like padding-block and padding-inline. It’s even possible to shave off just a few more bytes by selectively mixing and matching logical properties with traditional properties. I managed to get it down to 400 bytes with Brotli and 521 with gzip this way.

Other use cases

This seems most appropriate for things that are on a (linear) incremental scale (which is why padding and margin seems like a good use case) but I could see this potentially working for widths and heights in grid systems (column numbers and/or widths). Maybe for typographic scales (but maybe not).

I’ve focused a lot on file size, but there may be some other uses here I’m not thinking of. Perhaps you wouldn’t write your code in this way, but a critical CSS tool could potentially refactor the code to use this approach.

Digging deeper

As I dug deeper, I found that Ahmad Shadeed blogged in 2019 about mixing calc() with CSS variable assignments within inline styles particularly for avatar sizes. Miriam Suzanne’s article on Smashing Magazine in 2019 didn’t use calc() but shared some amazing things you can do with variable assignments in inline styles.


Efficient Infinite Utility Helpers Using Inline CSS Custom Properties and calc() originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/efficient-infinite-utility-helpers-using-inline-css-custom-properties-and-calc/feed/ 8 345467
Creating Stylesheet Feature Flags With Sass !default https://css-tricks.com/creating-stylesheet-feature-flags-with-sass-default/ https://css-tricks.com/creating-stylesheet-feature-flags-with-sass-default/#respond Fri, 14 May 2021 14:14:34 +0000 https://css-tricks.com/?p=340343 !default is a Sass flag that indicates conditional assignment to a variable — it assigns a value only if the variable was previously undefined or null. Consider this code snippet:

$variable: 'test' !default;

To the Sass compiler, this line …


Creating Stylesheet Feature Flags With Sass !default originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
!default is a Sass flag that indicates conditional assignment to a variable — it assigns a value only if the variable was previously undefined or null. Consider this code snippet:

$variable: 'test' !default;

To the Sass compiler, this line says:

Assign $variable to value 'test', but only if $variable is not already assigned.

Here’s the counter-example, illustrating the other side of the !default flag’s conditional behavior:

$variable: 'hello world';
$variable: 'test' !default;
// $variable still contains `hello world`

After running these two lines, the value of $variable is still 'hello world' from the original assignment on line 1. In this case, the !default assignment on line 2 is ignored since a value has already been provided, and no default value is needed.

Style libraries and @use...with

The primary motivation behind !default in Sass is to facilitate the usage of style libraries, and their convenient inclusion into downstream applications or projects. By specifying some of its variables as !default, the library can allow the importing application to customize or adjust these values, without completely forking the style library. In other words, !default variables essentially function as parameters which modify the behavior of the library code.

Sass has a special syntax just for this purpose, which combines a stylesheet import with its related variable overrides:

// style.scss
@use 'library' with (
  $foo: 'hello',
  $bar: 'world'
);

This statement functions almost the same as a variable assignment followed by an @import, like so:

// style.scss - a less idiomatic way of importing `library.scss` with configuration
$foo: 'hello';
$bar: 'world';
@import 'library';

The important distinction here, and the reason @use...with is preferable, is about the scope of the overrides. The with block makes it crystal clear — to both the Sass compiler and anyone reading the source code — that the overrides apply specifically to variables which are defined and used inside of library.scss. Using this method keeps the global scope uncluttered and helps mitigate variable naming collisions between different libraries.

Most common use case: Theme customization

// library.scss
$color-primary: royalblue !default;
$color-secondary: salmon !default: 


// style.scss
@use 'library' with (
  $color-primary: seagreen !default;
  $color-secondary: lemonchiffon !default: 
);

One of the most ubiquitous examples of this feature in action is the implementation of theming. A color palette may be defined in terms of Sass variables, with !default allowing customization of that color palette while all other styling remains the same (even including mixing or overlaying those colors).

Bootstrap exports its entire Sass variable API with the !default flag set on every item, including the theme color palette, as well as other shared values such as spacing, borders, font settings, and even animation easing functions and timings. This is one of the best examples of the flexibility provided by !default, even in an extremely comprehensive styling framework.

In modern web apps, this behavior by itself could be replicated using CSS Custom Properties with a fallback parameter. If your toolchain doesn’t already make use of Sass, modern CSS may be sufficient for the purposes of theming. However, we’ll examine use cases that can only be solved by use of the Sass !default flag in the next two examples.

Use case 2: Loading webfonts conditionally

// library.scss
$disable-font-cdn: false !default;
@if not $disable-font-cdn {
  @import url(''https://fonts.googleapis.com/css2?family=Public+Sans&display=swap'');
}


// style.scss
@use 'library' with (
  $disable-font-cdn: true
);
// no external HTTP request is made

Sass starts to show its strength when it leverages its preprocessor appearance in the CSS lifecycle. Suppose the style library for your company’s design system makes use of a custom webfont. It’s loaded from a Google’s CDN — ideally as quick as it gets — but nevertheless a separate mobile experience team at your company has concerns about page load time; every millisecond matters for their app.

To solve this, you can introduce an optional boolean flag in your style library (slightly different from the CSS color values from the first example). With the default value set to false, you can check this feature flag in a Sass @if statement before running expensive operations such as external HTTP requests. Ordinary consumers of your library don’t even need to know that the option exists — the default behavior works for them and they automatically load the font from the CDN, while other teams have access to the toggle what they need in order to fine tune and optimize page loading.

A CSS variable would not be sufficient to solve this problem — although the font-family could be overridden, the HTTP request would have already gone out to load the unused font.

Use case 3: Visually debugging spacing tokens

View live demo

!default feature flags can also be used to create debugging tools for use during development. In this example, a visual debugging tool creates color-coded overlays for spacing tokens. The foundation is a set of spacing tokens defined in terms of ascending “t-shirt sizes” (aka “xs”/”extra-small” through “xl”/”extra-large”). From this single token set, a Sass @each loop generates every combination of utility classes applying that particular token to padding or margin, on every side (top, right, bottom, and left individually, or all four at once).

Since these selectors are all constructed dynamically in a nested loop, and single !default flag can switch the rendering behavior from standard (margin plus padding) to the colored debug view (using transparent borders with the same sizes). This color-coded view may look very similar to the deliverables and wireframes which a designer might hand off for development — especially if you are already sharing the same spacing values between design and dev. Placing the visual debug view side-by-side with the mockup can help quickly and intuitively spot discrepancies, as well as debug more complex styling issues, such as margin collapse behavior.

Again — by the time this code is compiled for production, none of the debugging visualization will be anywhere in the resulting CSS since it will be completely replaced by the corresponding margin or padding statement.

Further reading

These are just a few examples of Sass !default in the wild. Refer to these documentation resources and usage examples as you adapt the technique to your own variations.


Creating Stylesheet Feature Flags With Sass !default originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-stylesheet-feature-flags-with-sass-default/feed/ 0 340343
Comparing Styling Methods in 2020 https://css-tricks.com/comparing-styling-methods-in-2020/ https://css-tricks.com/comparing-styling-methods-in-2020/#comments Mon, 19 Oct 2020 20:36:47 +0000 https://css-tricks.com/?p=323373 Over on Smashing, Adebiyi Adedotun Lukman covers all these styling methods. It’s in the context of Next.js, which is somewhat important as Next.js has some specific ways you work with these tools, is React and, thus, is a components-based …


Comparing Styling Methods in 2020 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Over on Smashing, Adebiyi Adedotun Lukman covers all these styling methods. It’s in the context of Next.js, which is somewhat important as Next.js has some specific ways you work with these tools, is React and, thus, is a components-based architecture. But the styling methods talked about transcend Next.js, and can apply broadly to lots of websites.

Here are my hot takes on the whole table-of-contents of styling possibilities these days.

  • Regular CSS. If you can, do. No build tooling is refreshing. It will age well. The only thing I really miss without any other tooling is nesting media queries within selector blocks.
  • Sass. Sass has been around the block and is still a hugely popular CSS preprocessor. Sass is built into tons of other tools, so choosing Sass doesn’t always mean the same thing. A simple Sass integration might be as easy as a sass --watch src/style.scss dist/style.css npm script. Then, once you’ve come to terms with the fact that you have a build process, you can start concatenating files, minifying, busting cache, and all this stuff that you’re probably going to end up doing somehow anyway.
  • Less & Stylus. I’m surprised they aren’t more popular since they’ve always been Node and work great with the proliferation of Node-powered build processes. Not to mention they are nice feature-rich preprocessors. I have nothing against either, but Sass is more ubiquitous, more actively developed, and canonical Sass now works fine in Node-land,
  • PostCSS. I’m not compelled by PostCSS because I don’t love having to cobble together the processing features that I want. That also has the bad side effect of making the process of writing CSS different across projects. Plus, I don’t like the idea of preprocessing away modern features, some of which can’t really be preprocessed (e.g. custom properties cannot be preprocessed). But I did love Autoprefixer when we really needed that, which is built on PostCSS.
  • CSS Modules. (Built on PostCSS). If you’re working with components in any technology, CSS modules give you the ability to scope CSS to that component, which is an incredibly great idea. I like this approach wherever I can get it. Your module CSS can be Sass too, so we can get the best of both worlds there.
  • CSS-in-JS. Let’s be real, this means “CSS-in-React.” If you’re writing Vue, you’re writing styles the way Vue helps you do it. Same with Svelte. Same with Angular. React is the un-opinionated one, leaving you to choose between things like styled-components, styled-jsx, Emotion… there are a lot of them. I have projects in React and I just use Sass+CSS Modules and think that’s good but a lot of people like CSS-in-JS approaches too. I get it. You get the scoping automatically and can do fancy stuff like incorporate props into styling decisions. Could be awesome for a design system.
  • All-in on utility styles: Big advantages: small CSS files, consistent yet flexible styles. Big disadvantage: you’ve got a zillion classes all commingled in your markup, making it cumbersome to read and refactor. I’m not compelled by it, but I get it, those advantages really hit for some folks.

If you want to hear some other hot takes on this spectrum, the Syntax FM fellas sounded off on this recently.


Comparing Styling Methods in 2020 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/comparing-styling-methods-in-2020/feed/ 10 323373
When Sass and New CSS Features Collide https://css-tricks.com/when-sass-and-new-css-features-collide/ https://css-tricks.com/when-sass-and-new-css-features-collide/#comments Mon, 29 Jun 2020 14:56:00 +0000 https://css-tricks.com/?p=312771 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 …


When Sass and New CSS Features Collide originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
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.”

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

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.”

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

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.”

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

The same thing happens for grayscale(): “$color: ‘calc(.2 + var(--d, .3))‘ is not a color for grayscale.”

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

opacity() causes the same issue: “$color: ‘var(--p, 0.8)‘ is not a color for opacity.”

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

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.

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.

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.

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.

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).

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.

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.

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

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.

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

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.

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

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.


When Sass and New CSS Features Collide originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/when-sass-and-new-css-features-collide/feed/ 16 312771
Making My Netlify Build Run Sass https://css-tricks.com/making-my-netlify-build-run-sass/ https://css-tricks.com/making-my-netlify-build-run-sass/#comments Tue, 09 Jun 2020 15:10:02 +0000 https://css-tricks.com/?p=312530 Let’s say you wanted to build a site with Eleventy as the generator. Popular choice these days! Eleventy doesn’t have some particularly blessed way of preprocessing your CSS, if that’s something you want to do. There are a variety of …


Making My Netlify Build Run Sass originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s say you wanted to build a site with Eleventy as the generator. Popular choice these days! Eleventy doesn’t have some particularly blessed way of preprocessing your CSS, if that’s something you want to do. There are a variety of ways to do it and perhaps that freedom is part of the spirit of Eleventy.

I’ve seen people set up Gulp for this, which is cool, I still use and like Gulp for some stuff. I’ve seen someone use templating to return preprocessed CSS, which seems weird, but hey, whatever works. I’ve even seen someone extend the Eleventy config itself to run the processing.

So far, the thing that has made the most sense to me is to use npm scripts do the Sass processing. Do the CSS first, then the HTML, with npm-run-all. So, you’d set up something like this in your package.json:

  "scripts": {
    "build": "npm-run-all build:css build:html",
    "build:css": "node-sass src/site/_includes/css/main.scss > src/site/css/main.css",
    "build:html": "eleventy",
    "watch": "npm-run-all --parallel watch:css watch:html",
    "watch:css": "node-sass --watch src/site/_includes/css/main.scss > src/site/css/main.css",
    "watch:html": "eleventy --serve --port=8181",
    "start": "npm run watch"
  },

I think that’s fairly nice. Since Eleventy doesn’t have a blessed CSS processing route anyway, it feels OK to have it de-coupled from Eleventy processing.

But I see Netlify has come along nicely with their build plugins. As Sarah put it:

What the Build Plugin does is give you access to key points in time during that process, for instance, onPreBuildonPostBuildonSuccess, and so forth. You can execute some logic at those specific points in time

There is something really intuitive and nice about that structure. A lot of build plugins are created by the community or Netlify themselves. You just click them on via the UI or reference them in your config. But Sass isn’t a build-in project (as I write), which I would assume is because people are a pretty opinionated about what/where/how their CSS is processed that it makes sense to just let people do it themselves. So let’s do that.

In our project, we’d create a directory for our plugins, and then a folder for this particular plugin we want to write:

project-root/
  src/
  whatever/
  plugins/
    sass/
      index.js
      manifest.yml

That index.js file is where we write our code, and we’ll specifically want to use the onPreBuild hook here, because we’d want our Sass to be done preprocessing before the build process runs Eleventy and Eleventy moves things around.

module.exports = {
  onPreBuild: async ({ utils: { run } }) => {
    await run.command(
      "node-sass src/site/_includes/css/main.scss src/site/css/main.css"
    );
  },
};

Here’s a looksie into all the relevant files together:

Now, if I netlify build from the command line, it will run the same build process that Netlify itself does, and it will hook into my plugin and run it!

One little thing I noticed is that I was trying to have my config be the (newer) netlify.yml format, but the plugins didn’t work, and I had to re-do the config as netlify.toml.

So we de-coupled ourselves from Eleventy with this particular processing, and coupled ourselves to Netlify. Just something to be aware of. I’m down with that as this way of configuring a build is so nice and I see so much potential in it.

I prefer the more explicit and broken up configuration of this style. Just look at how much cleaner the package.json gets:

Removed a bunch of lines from the scripts area of a package.json file, like the specific build:css and build:html commands

I still have this idea…

…of building a site that is a dog-fooded example of all the stuff you could/should do during a build process. I’ve started the site here, (and repo), but it’s not doing too much yet. I think it would be cool to wire up everything on that list (and more?) via Build Plugins.

If you wanna contribute, feel free to let me know. Maybe email me or open an issue to talk about what you’d want to do. You’re free to do a Pull Request too, but PRs without any prior communication are a little tricky sometimes as it’s harder to ensure our visions are aligned before you put in a bunch of effort.


Making My Netlify Build Run Sass originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-my-netlify-build-run-sass/feed/ 6 312530
How to Tame Line Height in CSS https://css-tricks.com/how-to-tame-line-height-in-css/ https://css-tricks.com/how-to-tame-line-height-in-css/#comments Fri, 15 May 2020 14:46:29 +0000 https://css-tricks.com/?p=308275 In CSS, line-height is probably one of the most misunderstood, yet commonly-used attributes. As designers and developers, when we think about line-height, we might think about the concept of leading from print design — a term, interestingly enough, that …


How to Tame Line Height in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In CSS, line-height is probably one of the most misunderstood, yet commonly-used attributes. As designers and developers, when we think about line-height, we might think about the concept of leading from print design — a term, interestingly enough, that comes from literally putting pieces of lead between lines of type. 

Leading and line-height, however similar, have some important differences. To understand those differences, we first have to understand a bit more about typography. 

An overview of typography terms

In traditional Western type design, a line of text is comprised of several parts: 

  • Baseline: This is the imaginary line on which the type sits. When you write in a ruled notebook, the baseline is the line on which you write.
  • Descender: This line sits just below the baseline. It is the line that some characters — like lowercase g, j, q, y and p  — touch below the baseline. 
  • X-height: This is (unsurprisingly) the height of a normal, lowercase x in a line of text. Generally, this is the height of other lowercase letters, although some may have parts of their characters that will exceed the x-height. For all intents and purposes, it serves as the perceived height of lowercase letters.
  • Cap-height: This is the height of most capital letters on a given line of text.
  • Ascender: A line that oftentimes appears just above the cap height where some characters like a lowercase h or b might exceed the normal cap height.
Illustrating the ascender, cap height, x-height, baseline and descender of the Lato font with The quick fox as sample text.

Each of the parts of text described above are intrinsic to the font itself. A font is designed with each of these parts in mind; however, there are some parts of typography that are left up to the type setter (like you and me!) rather than the designer. One of these is leading.

Leading is defined as the distance between two baselines in a set of type.

Two lines of text with an order box around the second line ofd text indicating the leading.

A CSS developer might think, “OK, leading is the line-height, let’s move on.” While the two are related, they are also different in some very important ways.

Let’s take a blank document and add a classic “CSS reset” to it:

* {
  margin: 0;
  padding: 0;
}

This removes the margin and padding from every single element.

We’ll also use Lato from Google Fonts as our font-family.

We will need some content, so let’s an create an <h1> tag with some text and set the line-height to something obnoxiously huge, like 300px. The result is a single line of text with a surprising amount of space both above and below the single line of text.

When a browser encounters the line-height property, what it actually does is take the line of text and place it in the middle of a “line box” which has a height matching the element’s line-height. Instead of setting the leading on a font, we get something akin to padding one either side of the line box.

Two lines of text with orange borders around each line of text, indicating the line box for each line. The bottom border of the first line and the top border of the second line are touching.

As illustrated above, the line box wraps around a line of text where leading is created by using space below one line of text and above the next. This means that for every text element on a page there will be half of the leading above the first line of text and after the last line of text in a particular text block.

What might be more surprising is that explicitly setting the line-height and font-size on an element with the same value will leave extra room above and below the text. We can see this by adding a background color to our elements.

This is because even though the font-size is set to 32px, the actual text size is something less than that value because of the generated spacing.

Getting CSS to treat line-height like leading

If we want CSS to use a more traditional type setting style instead of the line box, we’ll want a single line of text to have no space either above or below it — but allow for multi-line elements to maintain their entire line-height value. 

It is possible to teach CSS about leading with a little bit of effort. Michael Taranto released a tool called Basekick that solves this very issue. It does so by applying a negative top margin to the ::before pseudo-elementand a translateY to the element itself. The end result is a line of text without any extra space around it.

The most up-to-date version of Basekick’s formula can be found in the source code for the Braid Design System from SEEK. In the example below, we are writing a Sass mixin to do the heavy lifting for us, but the same formula can be used with JavaScript, Less, PostCSS mixins, or anything else that provides these kinds of math features.

@function calculateTypeOffset($lh, $fontSize, $descenderHeightScale) {
  $lineHeightScale: $lh / $fontSize;
  @return ($lineHeightScale - 1) / 2 + $descenderHeightScale;
}


@mixin basekick($typeSizeModifier, $baseFontSize, $descenderHeightScale, $typeRowSpan, $gridRowHeight, $capHeight) {
  $fontSize: $typeSizeModifier * $baseFontSize;
  $lineHeight: $typeRowSpan * $gridRowHeight;
  $typeOffset: calculateTypeOffset($lineHeight, $fontSize, $descenderHeightScale);
  $topSpace: $lineHeight - $capHeight * $fontSize;
  $heightCorrection: 0;
  
  @if $topSpace > $gridRowHeight {
    $heightCorrection: $topSpace - ($topSpace % $gridRowHeight);
  }
  
  $preventCollapse: 1;
  
  font-size: #{$fontSize}px;
  line-height: #{$lineHeight}px;
  transform: translateY(#{$typeOffset}em);
  padding-top: $preventCollapse;


  &::before {
    content: "";
    margin-top: #{-($heightCorrection + $preventCollapse)}px;
    display: block;
    height: 0;
  }
}

At first glance, this code definitely looks like a lot of magic numbers cobbled together. But it can be broken down considerably by thinking of it in the context of a particular system. Let’s take a look at what we need to know:

  • $baseFontSize:This is the normal font-size for our system around which everything else will be managed. We’ll use 16px as the default value.
  • $typeSizeModifier: This is a multiplier that is used in conjunction with the base font size to determine the font-size rule. For example, a value of 2 coupled with our base font size of 16px will give us font-size: 32px.
  • $descenderHeightScale: This is the height of the font’s descender expressed as a ratio. For Lato, this seems to be around 0.11.
  • $capHeight: This is the font’s specific cap height expressed as a ratio. For Lato, this is around 0.75.
  • $gridRowHeight: Layouts generally rely on default a vertical rhythm to make a nice and consistently spaced reading experience. For example, all elements on a page might be spaced apart in multiples of four or five pixels. We’ll be using 4 as the value because it divides easily into our $baseFontSize of 16px.
  • $typeRowSpan: Like $typeSizeModifier, this variable serves as a multiplier to be used with the grid row height to determine the rule’s line-height value. If our default grid row height is 4 and our type row span is 8, that would leave us with line-height: 32px.

Now we can then plug those numbers into the Basekick formula above (with the help of SCSS functions and mixins) and that will give us the result below.

That’s just what we’re looking for. For any set of text block elements without margins, the two elements should bump against each other. This way, any margins set between the two elements will be pixel perfect because they won’t be fighting with the line box spacing.

Refining our code

Instead of dumping all of our code into a single SCSS mixin, let’s organize it a bit better. If we’re thinking in terms of systems, will notice that there are three types of variables we are working with:

Variable TypeDescriptionMixin Variables
System LevelThese values are properties of the design system we’re working with.$baseFontSize
$gridRowHeight
Font LevelThese values are intrinsic to the font we’re using. There might be some guessing and tweaking involved to get the perfect numbers.$descenderHeightScale
$capHeight
Rule LevelThese values will are specific to the CSS rule we’re creating$typeSizeMultiplier
$typeRowSpan

Thinking in these terms will help us scale our system much easier. Let’s take each group in turn.

First off, the system level variables can be set globally as those are unlikely to change during the course of our project. That reduces the number of variables in our main mixin to four:

$baseFontSize: 16;
$gridRowHeight: 4;

@mixin basekick($typeSizeModifier, $typeRowSpan, $descenderHeightScale, $capHeight) {
  /* Same as above */
}

We also know that the font level variables are specific to their given font family. That means it would be easy enough to create a higher-order mixin that sets those as constants:

@mixin Lato($typeSizeModifier, $typeRowSpan) {
  $latoDescenderHeightScale: 0.11;
  $latoCapHeight: 0.75;
  
  @include basekick($typeSizeModifier, $typeRowSpan, $latoDescenderHeightScale, $latoCapHeight);
  font-family: Lato;
}

Now, on a rule basis, we can call the Lato mixin with little fuss:

.heading--medium {
  @include Lato(2, 10);
}

That output gives us a rule that uses the Lato font with a font-size of 32px and a line-height of 40px with all of the relevant translates and margins. This allows us to write simple style rules and utilize the grid consistency that designers are accustomed to when using tools like Sketch and Figma.

As a result, we can easily create pixel-perfect designs with little fuss. See how well the example aligns to our base 4px grid below. (You’ll likely have to zoom in to see the grid.)

Doing this gives us a unique superpower when it comes to creating layouts on our websites: We can, for the first time in history, actually create pixel-perfect pages. Couple this technique with some basic layout components and we can begin creating pages in the same way we would in a design tool.

Moving toward a standard

While teaching CSS to behave more like our design tools does take a little effort, there is potentially good news on the horizon. An addition to the CSS specification has been proposed to toggle this behavior natively. The proposal, as it stands now, would add an additional property to text elements similar to line-height-trim or leading-trim

One of the amazing things about web languages is that we all have an ability to participate. If this seems like a feature you would like to see as part of CSS, you have the ability to drop in and add a comment to that thread to let your voice be heard.


How to Tame Line Height in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/how-to-tame-line-height-in-css/feed/ 18 308275
Getting JavaScript to Talk to CSS and Sass https://css-tricks.com/getting-javascript-to-talk-to-css-and-sass/ https://css-tricks.com/getting-javascript-to-talk-to-css-and-sass/#comments Fri, 03 Apr 2020 15:05:57 +0000 https://css-tricks.com/?p=305435 JavaScript and CSS have lived beside one another for upwards of 20 years. And yet it’s been remarkably tough to share data between them. There have been large attempts, sure. But, I have something simple and intuitive in mind — …


Getting JavaScript to Talk to CSS and Sass originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
JavaScript and CSS have lived beside one another for upwards of 20 years. And yet it’s been remarkably tough to share data between them. There have been large attempts, sure. But, I have something simple and intuitive in mind — something not involving a structural change, but rather putting CSS custom properties and even Sass variables to use.

CSS custom properties and JavaScript

Custom properties shouldn’t be all that surprising here. One thing they’ve always been able to do since browsers started supporting them is work alongside JavaScript to set and manipulate the values.

Specifically, though, we can use JavaScript with custom properties in a few ways. We can set the value of a custom property using setProperty:

document.documentElement.style.setProperty("--padding", 124 + "px"); // 124px

We can also retrieve CSS variables using getComputedStyle in JavaScript. The logic behind this is fairly simple: custom properties are part of the style, therefore, they are part of computed style.

getComputedStyle(document.documentElement).getPropertyValue('--padding') // 124px

Same sort of deal with getPropertyValue. That let us get the custom property value from an inlined style from HTML markup.

document.documentElement.style.getPropertyValue("--padding'"); // 124px

Note that custom properties are scoped. This means we need to get computed styles from a particular element. As we previously defined our variable in :root we get them on the HTML element.

Sass variables and JavaScript

Sass is a pre-processing language, meaning it’s turned into CSS before it ever is a part of a website. For that reason, accessing them from JavaScript in the same way as CSS custom properties — which are accessible in the DOM as computed styles — is not possible. 

We need to modify our build process to change this. I doubt there isn’t a huge need for this in most cases since loaders are often already part of a build process. But if that’s not the case in your project, we need three modules that are capable of importing and translating Sass modules.

Here’s how that looks in a webpack configuration:

module.exports = {
 // ...
 module: {
  rules: [
   {
    test: /\.scss$/,
    use: ["style-loader", "css-loader", "sass-loader"]
   },
   // ...
  ]
 }
};

To make Sass (or, specifically, SCSS in this case) variables available to JavaScript, we need to “export” them.

// variables.scss
$primary-color: #fe4e5e;
$background-color: #fefefe;
$padding: 124px;

:export {
  primaryColor: $primary-color;
  backgroundColor: $background-color;
  padding: $padding;
}

The :export block is the magic sauce webpack uses to import the variables. What is nice about this approach is that we can rename the variables using camelCase syntax and choose what we expose.

Then we import the Sass file (variables.scss) file into JavaScript, giving us access to the variables defined in the file.

import variables from './variables.scss';

/*
 {
  primaryColor: "#fe4e5e"
  backgroundColor: "#fefefe"
  padding: "124px"
 }
*/

document.getElementById("app").style.padding = variables.padding;

There are some restrictions on the :export syntax that are worth calling out:

  • It must be at the top level but can be anywhere in the file.
  • If there is more than one in a file, the keys and values are combined and exported together.
  • If a particular exportedKey is duplicated, the last one (in the source order) takes precedence.
  • An exportedValue may contain any character that’s valid in CSS declaration values (including spaces).
  • An exportedValue does not need to be quoted because it is already treated as a literal string.

There are lots of ways having access to Sass variables in JavaScript can come in handy. I tend to reach for this approach for sharing breakpoints. Here is my breakpoints.scs file, which I later import in JavaScript so I can use the matchMedia() method to have consistent breakpoints.

// Sass variables that define breakpoint values
$breakpoints: (
  mobile: 375px,
  tablet: 768px,
  // etc.
);

// Sass variables for writing out media queries
$media: (
  mobile: '(max-width: #{map-get($breakpoints, mobile)})',
  tablet: '(max-width: #{map-get($breakpoints, tablet)})',
  // etc.
);

// The export module that makes Sass variables accessible in JavaScript
:export {
  breakpointMobile: unquote(map-get($media, mobile));
  breakpointTablet: unquote(map-get($media, tablet));
  // etc.
}

Animations are another use case. The duration of an animation is usually stored in CSS, but more complex animations need to be done with JavaScript’s help.

// animation.scss
$global-animation-duration: 300ms;
$global-animation-easing: ease-in-out;

:export {
  animationDuration: strip-unit($global-animation-duration);
  animationEasing: $global-animation-easing;
}

Notice that I use a custom strip-unit function when exporting the variable. This allows me to easily parse things on the JavaScript side.

// main.js
document.getElementById('image').animate([
  { transform: 'scale(1)', opacity: 1, offset: 0 },
  { transform: 'scale(.6)', opacity: .6, offset: 1 }
], {
  duration: Number(variables.animationDuration),
  easing: variables.animationEasing,
});

It makes me happy that I can exchange data between CSS, Sass and JavaScript so easily. Sharing variables like this makes code simple and DRY.

There are multiple ways to achieve the same sort of thing, of course. Les James shared an interesting approach in 2017 that allows Sass and JavaScript to interact via JSON. I may be biased, but I find the approach we covered here to be the simplest and most intuitive. It doesn’t require crazy changes to the way you already use and write CSS and JavaScript.

Are there other approaches that you might be using somewhere? Share them here in the comments — I’d love to see how you’re solving it.


Getting JavaScript to Talk to CSS and Sass originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/getting-javascript-to-talk-to-css-and-sass/feed/ 13 305435
Sass !default and themeable design systems https://css-tricks.com/sass-default-and-themeable-design-systems/ https://css-tricks.com/sass-default-and-themeable-design-systems/#comments Wed, 11 Mar 2020 14:11:05 +0000 https://css-tricks.com/?p=304844 This is a great blog post from Brad Frost where he walks us through an interesting example. Let’s say we’re making a theme and we have some Sass like this:

.c-text-input {
  background-color: $form-background-color;
  padding: 10px
}

If the $form-background-color


Sass !default and themeable design systems originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
This is a great blog post from Brad Frost where he walks us through an interesting example. Let’s say we’re making a theme and we have some Sass like this:

.c-text-input {
  background-color: $form-background-color;
  padding: 10px
}

If the $form-background-color variable isn’t defined then we don’t want the background-color to be outputted in our CSS at all. In fact, we want our output to look like this instead:

.c-text-input {
  padding: 10px;
}

See? No background-color property. As Brad shows, that’s possible today with Sass’s !default flag. You can use it like this as you’re setting up the variable:

$form-background-color: null !default;

.c-text-input {
  background-color: $form-background-color; /* this line won’t exist in the outputted CSS file */
  padding: 10px;
}

$form-background-color: red;

.c-text-input-fancy {
  background-color: $form-background-color; /* this line will be “red” because we defined the variable */
  padding: 10px;
}

It’s a really useful thing to remember if you want to ensure your CSS is as small as it can be while you’re creating complex themes with Sass.

To Shared LinkPermalink on CSS-Tricks


Sass !default and themeable design systems originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/sass-default-and-themeable-design-systems/feed/ 2 304844
A Handy Sass-Powered Tool for Making Balanced Color Palettes https://css-tricks.com/a-handy-sass-powered-tool-for-making-balanced-color-palettes/ https://css-tricks.com/a-handy-sass-powered-tool-for-making-balanced-color-palettes/#comments Mon, 09 Dec 2019 16:03:11 +0000 https://css-tricks.com/?p=299651 For those who may not come from a design background, selecting a color palette is often based on personal preferences. Choosing colors might be done with an online color tool, sampling from an image, “borrowing” from favorite brands, or just …


A Handy Sass-Powered Tool for Making Balanced Color Palettes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
For those who may not come from a design background, selecting a color palette is often based on personal preferences. Choosing colors might be done with an online color tool, sampling from an image, “borrowing” from favorite brands, or just sort of randomly picking from a color wheel until a palette “just feels right.”

Our goal is to better understand what makes a palette “feel right” by exploring key color attributes with Sass color functions. By the end, you will become more familiar with:

  • The value of graphing a palette’s luminance, lightness, and saturation to assist in building balanced palettes
  • The importance of building accessible contrast checking into your tools
  • Advanced Sass functions to extend for your own explorations, including a CodePen you can manipulate and fork

What you’ll ultimately find, however, is that color on the web is a battle of hardware versus human perception.

What makes color graphing useful

You may be familiar with ways of declaring colors in stylesheets, such as RGB and RGBA values, HSL and HSLA values, and HEX codes.

rbg(102,51,153)
rbga(102,51,153, 0.6)
hsl(270, 50%, 40%)
hsla(270, 50%, 40%, 0.6)
#663399

Those values give devices instructions on how to render color. Deeper attributes of a color can be exposed programmatically and leveraged to understand how a color relates to a broader palette.

The value of graphing color attributes is that we get a more complete picture of the relationship between colors. This reveals why a collection of colors may or may not feel right together. Graphing multiple color attributes helps hint at what adjustments can be made to create a more harmonious palette. We’ll look into examples of how to determine what to change in a later section.

Two useful measurements we can readily obtain using built-in Sass color functions are lightness and saturation.

  • Lightness refers to the mix of white or black with the color.
  • Saturation refers to the intensity of a color, with 100% saturation resulting in the purest color (no grey present).
$color: rebeccapurple;

@debug lightness($color);
// 40%

@debug saturation($color);
// 50%;

However, luminance may arguably be the most useful color attribute. Luminance, as represented in our tool, is calculated using the WCAG formula which assumes an sRGB color space. Luminance is used in the contrast calculations, and as a grander concept, also aims to get closer to quantifying the human perception of relative brightness to assess color relationships. This means that a tighter luminance value range among a palette is likely to be perceived as more balanced to the human eye. But machines are fallible, and there are exceptions to this rule that you may encounter as you manipulate palette values. For more extensive information on luminance, and a unique color space called CIELAB that aims to even more accurately represent the human perception of color uniformity, see the links at the end of this article.

Additionally, color contrast is exceptionally important for accessibility, particularly in terms of legibility and distinguishing UI elements, which can be calculated programmatically. That’s important in that it means tooling can test for passing values. It also means algorithms can, for example, return an appropriate text color when passed in the background color. So our tool will incorporate contrast checking as an additional way to gauge how to adjust your palette.

The functions demonstrated in this project can be extracted for helping plan a contrast-safe design system palette, or baked into a Sass framework that allows defining a custom theme.

Sass as a palette building tool

Sass provides several traditional programming features that make it perfect for our needs, such as creating and iterating through arrays and manipulating values with custom functions. When coupled with an online IDE, like CodePen, that has real-time processing, we can essentially create a web app to solve specific problems such as building a color palette.

Here is a preview of the tool we’re going to be using:

See the Pen
Sass Color Palette Grapher
by Stephanie Eckles (@5t3ph)
on CodePen.

Features of the Sass palette builder

  • It outputs an aspect ratio-controlled responsive graph for accurate plot point placement and value comparing.
  • It leverages the result of Sass color functions and math calculations to correctly plot points on a 0–100% scale.
  • It generates a gradient to provide a more traditional “swatch” view.
  • It uses built-in Sass functions to extract saturation and lightness values.
  • It creates luminance and contrast functions (forked from Material Web Components in addition to linking in required precomputed linear color channel values).
  • It returns appropriate text color for a given background, with a settings variable to change the ratio used.
  • It provides functions to uniformly scale saturation and lightness across a given palette.

Using the palette builder

To begin, you may wish to swap from among the provided example palettes to get a feel for how the graph values change for different types of color ranges. Simply copy a palette variable name and swap it for $default as the value of the $palette variable which can be found under the comment SWAP THE PALETTE VARIABLE.

Next, try switching the $contrastThreshold variable value between the predefined ratios, especially if you are less familiar with ensuring contrast passes WCAG guidelines.

Then try to adjust the $palette-scale-lightness or $palette-scale-saturation values. Those feed into the palette function and uniformly scale those measurements across the palette (up to the individual color’s limit).

Finally, have a go at adding your own palette, or swap out some colors within the examples. The tool is a great way to explore Sass color functions to adjust particular attributes of a color, some of which are demonstrated in the $default palette.

Interpreting the graphs and creating balanced, accessible palettes

The graphing tool defaults to displaying luminance due to it being the most reliable indicator of a balanced palette, as we discussed earlier. Depending on your needs, saturation and lightness can be useful metrics on their own, but mostly they are signalers that can help point to what needs adjusting to bring a palette’s luminance more in alignment. An exception may be creating a lightness scale based on each value in your established palette. You can swap to the $stripeBlue example for that.

The $default palette is actually in need of adjustment to get closer to balanced luminance:

The $default palette’s luminance graph

A palette that shows well-balanced luminance is the sample from Stripe ($stripe):

The $stripe palette luminance graph

Here’s where the tool invites a mind shift. Instead of manipulating a color wheel, it leverages Sass functions to programmatically adjust color attributes.

Check the saturation graph to see if you have room to play with the intensity of the color. My recommended adjustment is to wrap your color value with the scale-color function and pass an adjusted $saturation value, e.g. example: scale-color(#41b880, $saturation: 60%). The advantage of scale-color is that it fluidly adjusts the value based on the given percent.

Lightness can help explain why two colors feel different by assigning a value to their brightness measured against mixing them with white or black. In the $default palette, the change-color function is used for purple to align it’s relative $lightness value with the computed lightness() of the value used for the red.

The scale-color function also allows bundling both an adjusted $saturation and $lightness value, which is often the most useful. Note that provided percents can be negative.

By making use of Sass functions and checking the saturation and lightness graphs, the $defaultBalancedLuminance achieves balanced luminance. This palette also uses the map-get function to copy values from the $default palette and apply further adjustments instead of overwriting them, which is handy for testing multiple variations such as perhaps a hue shift across a palette.

The $defaultBalancedLuminance luminance graph

Take a minute to explore other available color functions.

http://jackiebalzer.com/color offers an excellent web app to review effects of Sass and Compass color functions.

Contrast comes into play when considering how the palette colors will actually be used in a UI. The tool defaults to the AA contrast most appropriate for all text: 4.5. If you are building for a light UI, then consider that any color used on text should achieve appropriate contrast with white when adjusting against luminance, indicated by the center color of the plot point.

Tip: The graph is set up with a transparent background, so you can add a background rule on body if you are developing for a darker UI.

Further reading

Color is an expansive topic and this article only hits the aspects related to Sass functions. But to truly understand how to create harmonious color systems, I recommend the following resources:

  • Color Spaces – is a super impressive deep-dive with interactive models of various color spaces and how they are computed.
  • Understanding Colors and Luminance – A beginner-friendly overview from MDN on color and luminance and their relationship to accessibility.
  • Perpetually Uniform Color Spaces – More information on perceptually uniform color systems, with an intro the tool HSLuv that converts values from the more familiar HSL color space to the luminance-tuned CIELUV color space.
  • Accessible Color Systems – A case study from Stripe about their experience building an accessible color system by creating custom tooling (which inspired this exploration and article).
  • A Nerd’s Guide to Color on the Web – This is a fantastic exploration of the mechanics of color on the web, available right here on CSS-Tricks.
  • Tanaguru Contrast Finder – An incredible tool to help if you are struggling to adjust colors to achieve accessible contrast.
  • ColorBox – A web app from Lyft that further explores color scales through graphing.
  • Designing Systematic Colors – Describes Mineral UI‘s exceptional effort to create color ramps to support consistent theming via a luminance-honed palette.
  • How we designed the new color palettes in Tableau 10 – Tableau exposed features of their custom tool that helped them create a refreshed palette based on CIELAB, including an approachable overview of that color space.

A Handy Sass-Powered Tool for Making Balanced Color Palettes originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-handy-sass-powered-tool-for-making-balanced-color-palettes/feed/ 1 299651
Introducing Sass Modules https://css-tricks.com/introducing-sass-modules/ https://css-tricks.com/introducing-sass-modules/#comments Mon, 07 Oct 2019 14:24:56 +0000 https://css-tricks.com/?p=296858 Sass just launched a major new feature you might recognize from other languages: a module system. This is a big step forward for @import. one of the most-used Sass-features. While the current @import rule allows you to pull …


Introducing Sass Modules originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Sass just launched a major new feature you might recognize from other languages: a module system. This is a big step forward for @import. one of the most-used Sass-features. While the current @import rule allows you to pull in third-party packages, and split your Sass into manageable “partials,” it has a few limitations:

  • @import is also a CSS feature, and the differences can be confusing
  • If you @import the same file multiple times, it can slow down compilation, cause override conflicts, and generate duplicate output.
  • Everything is in the global namespace, including third-party packages – so my color() function might override your existing color() function, or vice versa.
  • When you use a function like color(). it’s impossible to know exactly where it was defined. Which @import does it come from?

Sass package authors (like me) have tried to work around the namespace issues by manually prefixing our variables and functions — but Sass modules are a much more powerful solution. In brief, @import is being replaced with more explicit @use and @forward rules. Over the next few years Sass @import will be deprecated, and then removed. You can still use CSS imports, but they won’t be compiled by Sass. Don’t worry, there’s a migration tool to help you upgrade!

Import files with @use

@use 'buttons';

The new @use is similar to @import. but has some notable differences:

  • The file is only imported once, no matter how many times you @use it in a project.
  • Variables, mixins, and functions (what Sass calls “members”) that start with an underscore (_) or hyphen (-) are considered private, and not imported.
  • Members from the used file (buttons.scss in this case) are only made available locally, but not passed along to future imports.
  • Similarly, @extends will only apply up the chain; extending selectors in imported files, but not extending files that import this one.
  • All imported members are namespaced by default.

When we @use a file, Sass automatically generates a namespace based on the file name:

@use 'buttons'; // creates a `buttons` namespace
@use 'forms'; // creates a `forms` namespace

We now have access to members from both buttons.scss and forms.scss — but that access is not transferred between the imports: forms.scss still has no access to the variables defined in buttons.scss. Because the imported features are namespaced, we have to use a new period-divided syntax to access them:

// variables: <namespace>.$variable
$btn-color: buttons.$color;
$form-border: forms.$input-border;

// functions: <namespace>.function()
$btn-background: buttons.background();
$form-border: forms.border();

// mixins: @include <namespace>.mixin()
@include buttons.submit();
@include forms.input();

We can change or remove the default namespace by adding as <name> to the import:

@use 'buttons' as *; // the star removes any namespace
@use 'forms' as f;

$btn-color: $color; // buttons.$color without a namespace
$form-border: f.$input-border; // forms.$input-border with a custom namespace

Using as * adds a module to the root namespace, so no prefix is required, but those members are still locally scoped to the current document.

Import built-in Sass modules

Internal Sass features have also moved into the module system, so we have complete control over the global namespace. There are several built-in modules — math, color, string, list, map, selector, and meta — which have to be imported explicitly in a file before they are used:

@use 'sass:math';
$half: math.percentage(1/2);

Sass modules can also be imported to the global namespace:

@use 'sass:math' as *;
$half: percentage(1/2);

Internal functions that already had prefixed names, like map-get or str-index. can be used without duplicating that prefix:

@use 'sass:map';
@use 'sass:string';
$map-get: map.get(('key': 'value'), 'key');
$str-index: string.index('string', 'i');

You can find a full list of built-in modules, functions, and name changes in the Sass module specification.

New and changed core features

As a side benefit, this means that Sass can safely add new internal mixins and functions without causing name conflicts. The most exciting example in this release is a sass:meta mixin called load-css(). This works similar to @use but it only returns generated CSS output, and it can be used dynamically anywhere in our code:

@use 'sass:meta';
$theme-name: 'dark';

[data-theme='#{$theme-name}'] {
  @include meta.load-css($theme-name);
}

The first argument is a module URL (like @use) but it can be dynamically changed by variables, and even include interpolation, like theme-#{$name}. The second (optional) argument accepts a map of configuration values:

// Configure the $base-color variable in 'theme/dark' before loading
@include meta.load-css(
  'theme/dark', 
  $with: ('base-color': rebeccapurple)
);

The $with argument accepts configuration keys and values for any variable in the loaded module, if it is both:

  • A global variable that doesn’t start with _ or - (now used to signify privacy)
  • Marked as a !default value, to be configured
// theme/_dark.scss
$base-color: black !default; // available for configuration
$_private: true !default; // not available because private
$config: false; // not available because not marked as a !default

Note that the 'base-color' key will set the $base-color variable.

There are two more sass:meta functions that are new: module-variables() and module-functions(). Each returns a map of member names and values from an already-imported module. These accept a single argument matching the module namespace:

@use 'forms';

$form-vars: module-variables('forms');
// (
//   button-color: blue,
//   input-border: thin,
// )

$form-functions: module-functions('forms');
// (
//   background: get-function('background'),
//   border: get-function('border'),
// )

Several other sass:meta functions — global-variable-exists(), function-exists(), mixin-exists(), and get-function() — will get additional $module arguments, allowing us to inspect each namespace explicitly.

Adjusting and scaling colors

The sass:color module also has some interesting caveats, as we try to move away from some legacy issues. Many of the legacy shortcuts like lighten(). or adjust-hue() are deprecated for now in favor of explicit color.adjust() and color.scale() functions:

// previously lighten(red, 20%)
$light-red: color.adjust(red, $lightness: 20%);

// previously adjust-hue(red, 180deg)
$complement: color.adjust(red, $hue: 180deg);

Some of those old functions (like adjust-hue) are redundant and unnecessary. Others — like lighten. darken. saturate. and so on — need to be re-built with better internal logic. The original functions were based on adjust(). which uses linear math: adding 20% to the current lightness of red in our example above. In most cases, we actually want to scale() the lightness by a percentage, relative to the current value:

// 20% of the distance to white, rather than current-lightness + 20
$light-red: color.scale(red, $lightness: 20%);

Once fully deprecated and removed, these shortcut functions will eventually re-appear in sass:color with new behavior based on color.scale() rather than color.adjust(). This is happening in stages to avoid sudden backwards-breaking changes. In the meantime, I recommend manually checking your code to see where color.scale() might work better for you.

Configure imported libraries

Third-party or re-usable libraries will often come with default global configuration variables for you to override. We used to do that with variables before an import:

// _buttons.scss
$color: blue !default;

// old.scss
$color: red;
@import 'buttons';

Since used modules no longer have access to local variables, we need a new way to set those defaults. We can do that by adding a configuration map to @use:

@use 'buttons' with (
  $color: red,
  $style: 'flat',
);

This is similar to the $with argument in load-css(). but rather than using variable-names as keys, we use the variable itself, starting with $.

I love how explicit this makes configuration, but there’s one rule that has tripped me up several times: a module can only be configured once, the first time it is used. Import order has always been important for Sass, even with @import. but those issues always failed silently. Now we get an explicit error, which is both good and sometimes surprising. Make sure to @use and configure libraries first thing in any “entrypoint” file (the central document that imports all partials), so that those configurations compile before other @use of the libraries.

It’s (currently) impossible to “chain” configurations together while keeping them editable, but you can wrap a configured module along with extensions, and pass that along as a new module.

Pass along files with @forward

We don’t always need to use a file, and access its members. Sometimes we just want to pass it along to future imports. Let’s say we have multiple form-related partials, and we want to import all of them together as one namespace. We can do that with @forward:

// forms/_index.scss
@forward 'input';
@forward 'textarea';
@forward 'select';
@forward 'buttons';

Members of the forwarded files are not available in the current document and no namespace is created, but those variables, functions, and mixins will be available when another file wants to @use or @forward the entire collection. If the forwarded partials contain actual CSS, that will also be passed along without generating output until the package is used. At that point it will all be treated as a single module with a single namespace:

// styles.scss
@use 'forms'; // imports all of the forwarded members in the `forms` namespace

Note: if you ask Sass to import a directory, it will look for a file named index or _index)

By default, all public members will forward with a module. But we can be more selective by adding show or hide clauses, and naming specific members to include or exclude:

// forward only the 'input' border() mixin, and $border-color variable
@forward 'input' show border, $border-color;

// forward all 'buttons' members *except* the gradient() function
@forward 'buttons' hide gradient;

Note: when functions and mixins share a name, they are shown and hidden together.

In order to clarify source, or avoid naming conflicts between forwarded modules, we can use as to prefix members of a partial as we forward:

// forms/_index.scss
// @forward "<url>" as <prefix>-*;
// assume both modules include a background() mixin
@forward 'input' as input-*;
@forward 'buttons' as btn-*;

// style.scss
@use 'forms';
@include forms.input-background();
@include forms.btn-background();

And, if we need, we can always @use and @forward the same module by adding both rules:

@forward 'forms';
@use 'forms';

That’s particularly useful if you want to wrap a library with configuration or any additional tools, before passing it along to your other files. It can even help simplify import paths:

// _tools.scss
// only use the library once, with configuration
@use 'accoutrement/sass/tools' with (
  $font-path: '../fonts/',
);
// forward the configured library with this partial
@forward 'accoutrement/sass/tools';

// add any extensions here...


// _anywhere-else.scss
// import the wrapped-and-extended library, already configured
@use 'tools';

Both @use and @forward must be declared at the root of the document (not nested), and at the start of the file. Only @charset and simple variable definitions can appear before the import commands.

Moving to modules

In order to test the new syntax, I built a new open source Sass library (Cascading Color Systems) and a new website for my band — both still under construction. I wanted to understand modules as both a library and website author. Let’s start with the “end user” experience of writing site styles with the module syntax…

Maintaining and writing styles

Using modules on the website was a pleasure. The new syntax encourages a code architecture that I already use. All my global configuration and tool imports live in a single directory (I call it config), with an index file that forwards everything I need:

// config/_index.scss
@forward 'tools';
@forward 'fonts';
@forward 'scale';
@forward 'colors';

As I build out other aspects of the site, I can import those tools and configurations wherever I need them:

// layout/_banner.scss
@use '../config';

.page-title {
  @include config.font-family('header');
}

This even works with my existing Sass libraries, like Accoutrement and Herman, that still use the old @import syntax. Since the @import rule will not be replaced everywhere overnight, Sass has built in a transition period. Modules are available now, but @import will not be deprecated for another year or two — and only removed from the language a year after that. In the meantime, the two systems will work together in either direction:

  • If we @import a file that contains the new @use/@forward syntax, only the public members are imported, without namespace.
  • If we @use or @forward a file that contains legacy @import syntax, we get access to all the nested imports as a single namespace.

That means you can start using the new module syntax right away, without waiting for a new release of your favorite libraries: and I can take some time to update all my libraries!

Migration tool

Upgrading shouldn’t take long if we use the Migration Tool built by Jennifer Thakar. It can be installed with Node, Chocolatey, or Homebrew:

npm install -g sass-migrator
choco install sass-migrator
brew install sass/sass/migrator

This is not a single-use tool for migrating to modules. Now that Sass is back in active development (see below), the migration tool will also get regular updates to help migrate each new feature. It’s a good idea to install this globally, and keep it around for future use.

The migrator can be run from the command line, and will hopefully be added to third-party applications like CodeKit and Scout as well. Point it at a single Sass file, like style.scss. and tell it what migration(s) to apply. At this point there’s only one migration called module:

# sass-migrator <migration> <entrypoint.scss...>
sass-migrator module style.scss

By default, the migrator will only update a single file, but in most cases we’ll want to update the main file and all its dependencies: any partials that are imported, forwarded, or used. We can do that by mentioning each file individually, or by adding the --migrate-deps flag:

sass-migrator --migrate-deps module style.scss

For a test-run, we can add --dry-run --verbose (or -nv for short), and see the results without changing any files. There are a number of other options that we can use to customize the migration — even one specifically for helping library authors remove old manual namespaces — but I won’t cover all of them here. The migration tool is fully documented on the Sass website.

Updating published libraries

I ran into a few issues on the library side, specifically trying to make user-configurations available across multiple files, and working around the missing chained-configurations. The ordering errors can be difficult to debug, but the results are worth the effort, and I think we’ll see some additional patches coming soon. I still have to experiment with the migration tool on complex packages, and possibly write a follow-up post for library authors.

The important thing to know right now is that Sass has us covered during the transition period. Not only can imports and modules work together, but we can create “import-only” files to provide a better experience for legacy users still @importing our libraries. In most cases, this will be an alternative version of the main package file, and you’ll want them side-by-side: <name>.scss for module users, and <name>.import.scss for legacy users. Any time a user calls @import <name>, it will load the .import version of the file:

// load _forms.scss
@use 'forms';

// load _forms.input.scss
@import 'forms';

This is particularly useful for adding prefixes for non-module users:

// _forms.import.scss
// Forward the main module, while adding a prefix
@forward "forms" as forms-*;

Upgrading Sass

You may remember that Sass had a feature-freeze a few years back, to get various implementations (LibSass, Node Sass, Dart Sass) all caught up, and eventually retired the original Ruby implementation. That freeze ended last year, with several new features and active discussions and development on GitHub – but not much fanfare. If you missed those releases, you can get caught up on the Sass Blog:

Dart Sass is now the canonical implementation, and will generally be the first to implement new features. If you want the latest, I recommend making the switch. You can install Dart Sass with Node, Chocolatey, or Homebrew. It also works great with existing gulp-sass build steps.

Much like CSS (since CSS3), there is no longer a single unified version-number for new releases. All Sass implementations are working from the same specification, but each one has a unique release schedule and numbering, reflected with support information in the beautiful new documentation designed by Jina.

Sass Modules are available as of October 1st, 2019 in Dart Sass 1.23.0.


Introducing Sass Modules originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/introducing-sass-modules/feed/ 17 296858
A Proof of Concept for Making Sass Faster https://css-tricks.com/a-proof-of-concept-for-making-sass-faster/ https://css-tricks.com/a-proof-of-concept-for-making-sass-faster/#comments Tue, 17 Sep 2019 14:19:18 +0000 https://css-tricks.com/?p=295393 At the start of a new project, Sass compilation happens in the blink of an eye. This feels great, especially when it’s paired with Browsersync, which reloads the stylesheet for us in the browser. But, as the amount of Sass …


A Proof of Concept for Making Sass Faster originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
At the start of a new project, Sass compilation happens in the blink of an eye. This feels great, especially when it’s paired with Browsersync, which reloads the stylesheet for us in the browser. But, as the amount of Sass grows, compilation time increases. This is far from ideal.

It can be a real pain when the compilation time creeps over one or two seconds. For me, that’s enough time to lose focus at the end of a long day. So, I would like to share a solution that’s available in a WordPress CSS Editor called Microthemer, as a proof of concept.

This is a two-part article. The first part is for the attention of Sass users. We will introduce the basic principles, performance results, and an interactive demo. The second part covers the nuts and bolts of how Microthemer makes Sass faster. And considers how to implement this as an npm package that can deliver fast, scalable Sass to a much broader community of developers.

How Microthemer compiles Sass in an instant

In some ways, this performance optimisation is simple. Microthemer just compiles less Sass code. It doesn’t intervene with Sass’ internal compilation process.

In order to feed the Sass compiler less code, Microthemer maintains an understanding of Sass entities used throughout the code base, like variables and mixins. When a selector is edited, Microthemer only compiles that single selector, plus any related selectors. Selectors are related if they make use of the same variables for instance, or one selector extends another. With this system, Sass compilation remains as fast with 3000 selectors as it does with a handful.

Performance results

With 3000 selectors, the compile time is around 0.05 seconds. It varies, of course. Sometimes it might be closer to 0.1 seconds. Other times the compilation happens as fast as 0.01 seconds (10ms).

To see for yourself, you can watch a video demonstration. Or mess around with the online Microthemer playground (see instructions below).

Online Microthemer playground

The online playground makes it easy to experiment with Microthemer yourself.

Instructions

  1. Go to the online Microthemer playground.
  2. Enable support for Sass via General → Preferences → CSS / SCSS → Enable SCSS.
  3. Go to View → full code editor → on to add global variables, mixins, and functions.
  4. Switch back to the main UI view (View → full code editor → off).
  5. Create selectors via the Target button.
  6. Add Sass code via the editor to the left of the Font property group.
  7. After each change, you can see what code Microthemer included in the compile process via View → Generated CSS → Previous SCSS compile.
  8. To see how this works at scale, you can import vanilla CSS from a large stylesheet into Microthemer via Packs → Import → CSS stylesheet (importing Sass isn’t supported – yet).

Do you want this as an npm package?

Microthemer’s selective compilation technique could also be delivered as an npm package. But the question is, do you see a need for this? Could your local Sass environment do with a speed boost? If so, please leave a comment below.

The rest of this article is aimed at those who develop tools for the community. As well as those who might be curious about how this challenge was tackled.

The Microthemer way to compile Sass

We will move on to some code examples shortly. But first, let’s consider the main application goals.

1. Compile minimal code

We want to compile the one selector being edited if it has no relationship with other selectors, or multiple selectors with related Sass entities — but no more than necessary.

2. Responsive to code changes

We want to remove any perception of waiting for Sass to compile. We also don’t want to crunch too much data between user keystrokes.

3. Equal CSS output

We want to return the same CSS a full compile would generate, but for a subset of code.

Sass examples

The following code will serve as a point of reference throughout this article. It covers all of the scenarios our selective compiler needs to handle. Such as global variables, mixin side-effects, and extended selectors.

Variables, functions, and mixins

$primary-color: green;
$secondary-color: red;
$dark-color: black;

@function toRem($px, $rootSize: 16){
  @return #{$px / $rootSize}rem;
}

@mixin rounded(){
  border-radius: 999px;
  $secondary-color: blue !global;
}

Selectors

.entry-title {
  color: $dark-color;
}

.btn {
  display: inline-block;
  padding: 1em;
  color: white;
  text-decoration: none;
}

.btn-success {
  @extend .btn;
  background-color: $primary-color;
  @include rounded;
}

.btn-error {
  @extend .btn;
  background-color: $secondary-color;
}

// Larger screens
@media (min-width: 960px) {
  .btn-success {
    border:4px solid darken($primary-color, 10%);
    &::before {
      content: "\2713"; // unicode tick
      margin-right: .5em;
    }
  }
}

The Microthemer interface

Microthemer has two main editing views.

View 1: Full code

We edit the full code editor in the same way as a regular Sass file. That’s where global variables, functions, mixins, and imports go.

View 2: Visual

The visual view has a single selector architecture. Each CSS selector is a separate UI selector. These UI selectors are organized into folders.

Because Microthemer segments individual selectors, analysis happens at a very granular scale — one selector at a time.

Here’s a quick quiz question for you. The $secondary-color variable is set to red at the top of the full code view. So why is the error button in the previous screenshots blue? Hint: it has to do with mixin side effects. More on that shortly.

Third party libraries

A huge thanks to the authors of the following JavaScript libraries Microthemer uses:

  • Gonzales PE – This converts Sass code to an abstract syntax tree (AST) JavaScript object.
  • Sass.js – This converts Sass to CSS code in the browser. It uses web workers to run the compilation on a separate thread.

Data objects

Now for the nitty gritty details. Figuring out an appropriate data structure took some trial and error. But once sorted, the application logic fell into place naturally. So we’ll start by explaining the main data stores, and then finish with a brief summary of the processing steps.

Microthemer uses four main JavaScript objects for storing application data.

  1. projectCode: Stores all project code partitioned into discreet items for individual selectors.
  2. projectEntities: Stores all variables, functions, mixins, extends and imports used in the project, as well as the locations of where these entities are used.
  3. connectedEntities: Stores the connections one piece of code has with project Sass entities.
  4. compileResources: Stores the selective compile data following a change to the code base.

projectCode

The projectCode object allows us to quickly retrieve pieces of Sass code. We then combine these pieces into a single string for compilation.

  • files: With Microthemer, this stores the code added to the full code view described earlier. With an npm implementation, fileswould relate to actual .sass or .scss system files.
  • folders: Microthemer’s UI folders that contain segmented UI selectors.
  • index: The order of a folder, or a selector within a folder.
  • itemData: The actual code for the item, explained further in the next code snippet.
var projectCode = {

  // Microthemer full code editor
  files: {
    full_code: {
      index: 0,
      itemData: itemData
    }
  },

  // Microthemer UI folders and selectors
  folders: {
    content_header: {
      index:100,
      selectors: {
        '.entry-title': {
          index:0,
            itemData: itemData
        },
      }
    },
    buttons: {
      index:200,
      selectors: {
        '.btn': {
          index:0,
          itemData: itemData
        },
        '.btn-success': {
          index:1,
          itemData: itemData
        },
        '.btn-error': {
          index:2,
          itemData: itemData
        }
      }
    }
  }
};

itemData for .btn-success selector

The following code example shows the itemData for the .btn-success selector.

  • sassCode: Used to build the compilation string.
  • compiledCSS: Stores compiled CSS for writing to a stylesheet or style node in the document head.
  • sassEntities: Sass entities for single selector or file. Allows for before and after change analysis, and is used to build the projectEntities object.
  • mediaQueries: Same data as above, but for a selector used inside a media query.
var itemData = {
  sassCode: ".btn-success { @extend .btn; background-color: $primary-color; @include rounded; }",
  compiledCSS: ".btn-success { background-color: green; border-radius: 999px; }",
  sassEntities: {
    extend: {
      '.btn': {
        values: ['.btn']
      }
    },
    variable: {
      primary_color: {
        values: [1]
      }
    },
    mixin: {
      rounded: {
        values: [1]
      }
    }
  },
  mediaQueries: {
    'min-width(960px)': {
      sassCode: ".btn-success { border:4px solid darken($primary-color, 10%); &::before { content: '\\2713'; margin-right: .5em; } }",
      compiledCSS: ".btn-success::before { content: '\\2713'; margin-right: .5em; }",
      sassEntities: {
        variable: {
          primary_color: {
            values: [1]
          }
        },
        function: {
          darken: {
            values: [1]
          }
        }
      }
    }
  }
};

projectEntities

The projectEntities object allows us to check which selectors use particular Sass entities.

  • variable, function, mixin, extend: The type of Sass entity.
  • E.g. primary_color: The Sass entity name. Microthemer normalizes hyphenated names because Sass uses hyphens and underscores interchangeably.
  • values: An array of declaration values or instances. Instances are represented by the number 1. The Gonzales PE Sass parser converts numeric declaration values to strings. So I’ve elected to use the integer 1 to flag instances.
  • itemDeps: An array of selectors that makes use of the Sass entity. This is explained further in the next code snippet.
  • relatedEntities: Our rounded mixin has a side effect of updating the global $secondary-color variable to blue, hence the blue error button. This side effect makes the rounded and $secondary-color entities co-dependent. So, when the $secondary-color variable is included, the roundedmixin should be included too, and vice versa.
var projectEntities = {
  variable: {
    primary_color: {
      values: ['green', 1],
      itemDeps: itemDeps
    },
    secondary_color: {
      values: ["red", "blue !global", 1],
      itemDeps: itemDeps,
      relatedEntities: {
        mixin: {
          rounded: {}
        }
      }
    },
    dark_color: {
      values: ["black", 1],
      itemDeps: itemDeps
    }
  },
  function: {
    darken: {
      values: [1]
    },
    toRem: {
      values: ["@function toRem($px, $rootSize: 16){↵   @return #{$px / $rootSize}rem;↵}", 1],
      itemDeps: itemDeps
    }
  },
  mixin: {
    rounded: {
      values: ["@mixin rounded(){↵   border-radius:999px;↵   $secondary-color: blue !global;↵}", 1],
      itemDeps: itemDeps,
      relatedEntities: {
        variable: {
          secondary_color: {
            values: ["blue !global"],
          }
        }
      }
    }
  },
  extend: {
    '.btn': {
      values: ['.btn', '.btn'],
        itemDeps: itemDeps
    }
  }
};

itemDeps for the $primary-color Sass entity

The following code example shows the itemDeps for the $primary-color (primary_color) variable. The $primary-color variable is used by two forms of the .btn-success selector, including a selector inside the min-width(960px) media query.

  • path: Used to retrieve selector data from the projectCode object.
  • mediaQuery: Used when updating style nodes or writing to a CSS stylesheet.
var itemDeps = [
  {
    path: ["folders", 'header', 'selectors', '.btn-success'],
  },
  {
    path: ["folders", 'header', 'selectors', '.btn-success', 'mediaQueries', 'min-width(960px)'],
    mediaQuery: 'min-width(960px)'
  }
];

connectedEntities

The connectedEntities object allows us to find related pieces of code. We populate it following a change to the code base. So, if we were to remove the font-size declaration from the .btn selector, the code would change from this:

.btn {
    display: inline-block;
    padding: 1em;
    color: white;
    text-decoration: none;
    font-size: toRem(21);
}

…to this:

.btn {
    display: inline-block;
    padding: 1em;
    color: white;
    text-decoration: none;
}

And we would store Microthemer’s analysis in the following connectedEntities object.

  • changed: The change analysis, which captures the removal of the toRem function.

    • actions: an array of user actions.
    • form: Declaration (e.g. $var: 18px) or instance (e.g. font-size: $var).
    • value: A text value for a declaration, or the integer 1 for an instance.
  • coDependent: Extended selectors must always compile with the extending selector, and vice versa. The relationship is co-dependent. Variables, functions, and mixins are only semi-dependent. Instances must compile with declarations, but declarations do not need to compile with instances. However, Microthemer treats them as co-dependent for the sake of simplicity. In the future, logic will be added to filter out unnecessary instances, but this has been left out for the first release.
  • related: the rounded mixin is related to the $secondary-color variable. It updates that variable using the global flag. The two entities are co-dependent; they should always compile together. But in our example, the .btn selector makes no use of the rounded mixin. So, the related property below is not populated with anything.
var connectedEntities = {
  changed: {
    function: {
      toRem: {
        actions: [{
          action: "removed",
          form: "instance",
          value: 1
        }]
      }
    }
  },
  coDependent: {
    extend: {
      '.btn': {}
    }
  },
  related: {}
};

compileResources

The compileResources object allows us to compile a subset of code in the correct order. In the previous section we removed the font-size declaration. The code below shows how the compileResources object would look after that change.

  • compileParts: An array of resources to be compiled.

    • path: Used to update the compiledCSS property of the relevant projectCodeitem.
    • sassCode: Used to build the sassString for compilation. We append a CSS comment to each piece (/*MTPART*/) . This comment is used to split the combined CSS output into the cssParts array.
  • sassString: A string of Sass code that compiles to CSS.
  • cssParts: CSS output in the form of an array. The array keys for cssParts line up with the compileParts array.
var compileResources = {

  compileParts: [
    {
      path: ["files", "full_code"],
      sassCode: "/*MTFILE*/$primary-color: green; $secondary-color: red; $dark-color: black; @function toRem($px, $rootSize: 16){ @return #{$px / $rootSize}rem; } @mixin rounded(){ border-radius:999px; $secondary-color: blue !global;}/*MTPART*/"
    },
    {
      path: ["folders", "buttons", ".btn"],
      sassCode: ".btn { display: inline-block; padding: 1em; color: white; text-decoration: none; }/*MTPART*/"
    },
    {
      path: ["folders", "buttons", ".btn-success"],
      sassCode: ".btn-success { @extend .btn; background-color: $primary-color; @include rounded; }/*MTPART*/"
    },
    {
      path: ["folders", "buttons", ".btn-error"],
      sassCode: ".btn-error { @extend .btn; background-color: $secondary-color; }/*MTPART*/"
    }
  ],

  sassString: 
  "/*MTFILE*/$primary-color: green; $secondary-color: red; $dark-color: black; @function toRem($px, $rootSize: 16){ @return #{$px / $rootSize}rem; } @mixin rounded(){ border-radius:999px; $secondary-color: blue !global;}/*MTPART*/"+
  ".btn { display: inline-block; padding: 1em; color: white; text-decoration: none;}/*MTPART*/"+
  ".btn-success {@extend .btn; background-color: $primary-color; @include rounded;}/*MTPART*/"+
  ".btn-error {@extend .btn; background-color: $secondary-color;}/*MTPART*/",

  cssParts: [
    "/*MTFILE*//*MTPART*/",
    ".btn, .btn-success, .btn-error { display: inline-block; padding: 1em; color: white; text-decoration: none;}/*MTPART*/",
    ".btn-success { background-color: green; border-radius: 999px;}/*MTPART*/",
    ".btn-error { background-color: blue;}/*MTPART*/"
  ]
};

Why were four resources included?

  1. full_code: The toRem Sass entity changed and the full_code resource contains the toRem function declaration.
  2. .btn: the selector was edited.
  3. .btn-success: Uses @extend .btn and so it must always compile with .btn. The combined selector becomes .btn, .btn-success.
  4. .btn-error: This also uses @extend .btn and so must be included for the same reasons as .btn-success.

Two selectors are not included because they are not related to the .btn selector.

  1. .entry-title
  2. .btn-success (inside the media query)

Recursive resource gathering

Aside from the data structure, the most time consuming challenge was figuring out how to pull in the right subset of Sass code. When one piece of code connects to another piece, we need to check for connections on the second piece too. There is a chain reaction. To support this, the following gatherCompileResources function is recursive.

  • We loop the connectedEntities object down to the level of Sass entity names.
  • We use recursion if a function or mixin has side effects (like updating global variables).
  • The checkObject function returns the value of an object at a particular depth, or false if no value exists.
  • The updateObject function sets the value of an object at a particular depth.
  • We add dependent resources to the compileParts array, using absoluteIndex as the key.
  • Microthemer calculates absoluteIndex by adding the folder index to the selector index. This works because folder indexes increment in hundreds, and the maximum number of selectors per folder is 40, which is fewer than one hundred.
  • We use recursion if resources added to the compileParts array also have co-dependent relationships.
function gatherCompileResources(compileResources, connectedEntities, projectEntities, projectCode, config){

  let compileParts = compileResources.compileParts;

  // reasons: changed / coDependent / related
  const reasons = Object.keys(connectedEntities);
  for (const reason of reasons) {

    // types: variable / function / mixin / extend
    const types = Object.keys(connectedEntities[reason]);
    for (const type of types) {

      // names: e.g. toRem / .btn / primary_color
      const names = Object.keys(connectedEntities[reason][type]);
      for (const name of names) {

        // check side-effects for Sass entity (if not checked already)
        if (!checkObject(config.relatedChecked, [type, name])){

          updateObject(config.relatedChecked, [type, name], 1);

          const relatedEntities = checkObject(projectEntities, [type, name, 'relatedEntities']);
          if (relatedEntities){
            compileParts = gatherCompileResources(
              compileResources, { related: relatedEntities }, projectEntities, projectCode, config
            );
          }
        }

        // check if there are dependent pieces of code
        const itemDeps = checkObject(projectEntities, [type, name, 'itemDeps']);
        if (itemDeps && itemDeps.length > 0){

          for (const dep of itemDeps) {

            let path = dep.path,
            resourceID = path.join('.');

            if (!config.resourceAdded[resourceID]){

              // if we have a valid resource
              let resource = checkObject(projectCode, path);
              if (resource){

                config.resourceAdded[resourceID] = 1;

                // get folder index + resource index
                let absoluteIndex = getAbsoluteIndex(path);

                // add compile part
                compileParts[absoluteIndex] = {
                  sassCode: resource.sassCode,
                  mediaQuery: resource.mediaQuery,
                  path: path
                };
                        
                // if resource is co-dependent, pull in others
                let coDependent = getCoDependent(resource);
                if (coDependent){
                  compileParts = gatherCompileResources(
                    compileResources, { coDependent: coDependent }, projectEntities, projectCode, config
                  );
                }
              }
            }
          }
        }
      }
    }
  }
  return compileParts;
}

The application process

We’ve covered the technical aspects now. To see how it all ties together, let’s walk through the data processing steps.

From keystroke to style render

  1. A user keystroke fires the textarea change event.
  2. We convert the single selector being edited into a sassEntities object. This allows for comparison with the pre-edit Sass entities: projectCode > dataItem > sassEntities.
  3. If any Sass entities changed:

    • We update projectCode > dataItem > sassEntities.
    • If an @extend rule changed:
      • We search the projectCode object to find matching selectors.
      • We store the path for matching selectors on the current data item: projectCode > dataItem > sassEntities > extend > target > [ path ].
    • We rebuild the projectEntities object by looping over the projectCode object.
    • We populate connectedEntities > changed with the change analysis.
    • If extend, variable, function, or mixin entities are present:

      • We populate connectedEntities > coDependent with the relevant entities.
  4. The recursive gatherCompileResources function uses the connectedEntities object to populate the compileResources object.
  5. We concatenate the compileResources > compileParts array into a single Sass string.
  6. We compile the single Sass string to CSS.
  7. We use comment dividers to split the output into an array: compileResources > cssParts. This array lines up with the compileResources > compileParts array via matching array keys.
  8. We use resource paths to update the projectCode object with compiled CSS.
  9. We write the CSS for a given folder or file to a style node in the document head for immediate style rendering. On the server-side, we write all CSS to a single stylesheet.

Considerations for npm

There are some extra considerations for an npm package. With a typical NodeJS development environment:

  • Users edit selectors as part of a larger file, rather than individually.
  • Sass imports are likely to play a larger role.

Segmentation of code

Microthemer’s visual view segments individual selectors. This makes parsing code to a sassEntities object super fast. Parsing whole files might be a different story, especially large ones.

Perhaps techniques exist for virtually segmenting system files? But suppose there is no way around parsing whole files. Or it just seems sensible for a first release. Maybe it’s sufficient to advise end users to keep Sass files small for best results.

Sass imports

At the time of writing, Microthemer doesn’t analyse import files. Instead, it includes all Sass imports whenever a selector makes use of any Sass entities. This is an interim first release solution that is okay in the context of Microthemer. But I believe an npm implementation should track Sass usage across all project files.

Our projectCode object already has a files property for storing file data. I propose calculating file indexes relative to the main Sass file. For instance, the file used in the first @import rule would have index: 0, the next, index: 1, and so on. We would need to scan @import rules within imported files to calculate these indexes correctly.

We would need to calculate absoluteIndex differently, too. Unlike Microthemer folders, system files can have any number of selectors. The compileParts array might need to be an object, storing an array of parts for each file. That way, we only keep track of selector indexes within a given file and then concatenate the compileParts arrays in the order of the files.

Conclusion

This article introduces a new way to compile Sass code, selectively. Near instant Sass compilation was necessary for Microthemer because it is a live CSS editor. And the word ‘live’ carries certain expectations of speed. But it may also be desirable for other environments, like Node.js. This is for Node.js and Sass users to decide, and hopefully share their thoughts in the comments below. If the demand is there, I hope an npm developer can take advantage of the points I’ve shared.

Please feel free to post any questions about this in my forum. I’m always happy to help.


A Proof of Concept for Making Sass Faster originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/a-proof-of-concept-for-making-sass-faster/feed/ 9 295393
Creating a Maintainable Icon System with Sass https://css-tricks.com/creating-a-maintainable-icon-system-with-sass/ https://css-tricks.com/creating-a-maintainable-icon-system-with-sass/#comments Wed, 28 Aug 2019 14:05:56 +0000 https://css-tricks.com/?p=294949 One of my favorite ways of adding icons to a site is by including them as data URL background images to pseudo-elements (e.g. ::after) in my CSS. This technique offers several advantages:

  • They don’t require any additional HTTP requests


Creating a Maintainable Icon System with Sass originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
One of my favorite ways of adding icons to a site is by including them as data URL background images to pseudo-elements (e.g. ::after) in my CSS. This technique offers several advantages:

  • They don’t require any additional HTTP requests other than the CSS file.
  • Using the background-size property, you can set your pseudo-element to any size you need without worrying that they will overflow the boundaries (or get chopped off).
  • They are ignored by screen readers (at least in my tests using VoiceOver on the Mac) so which is good for decorative-only icons.

But there are some drawbacks to this technique as well:

  • When used as a background-image data URL, you lose the ability to change the SVG’s colors using the “fill” or “stroke” CSS properties (same as if you used the filename reference, e.g. url( 'some-icon-file.svg' )). We can use filter() as an alternative, but that might not always be a feasible solution.
  • SVG markup can look big and ugly when used as data URLs, making them difficult to maintain when you need to use the icons in multiple locations and/or have to change them.

We’re going to address both of these drawbacks in this article.

The situation

Let’s build a site that uses a robust iconography system, and let’s say that it has several different button icons which all indicate different actions:

  • A “download” icon for downloadable content
  • An “external link” icon for buttons that take us to another website
  • A “right caret” icon for taking us to the next step in a process

Right off the bat, that gives us three icons. And while that may not seem like much, already I’m getting nervous about how maintainable this is going to be when we scale it out to more icons like social media networks and the like. For the sake of this article, we’re going to stop at these three, but you can imagine how in a sophisticated icon system this could get very unwieldy, very quickly.

It’s time to go to the code. First, we’ll set up a basic button, and then by using a BEM naming convention, we’ll assign the proper icon to its corresponding button. (At this point, it’s fair to warn you that we’ll be writing everything out in Sass, or more specifically, SCSS. And for the sake of argument, assume I’m running Autoprefixer to deal with things like the appearance property.)

.button {
  appearance: none;
  background: #d95a2b;
  border: 0;
  border-radius: 100em;
  color: #fff;
  cursor: pointer;
  display: inline-block;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
  position: relative;
  text-align: center;
  text-transform: uppercase;
  transition: background-color 200ms ease-in-out;

  &amp;:hover,
  &amp;:focus,
  &amp;:active {
    background: #8c3c2a;
  }
}

This gives us a simple, attractive, orange button that turns to a darker orange on the hover, focused, and active states. It even gives us a little room for the icons we want to add, so let’s add them in now using pseudo-elements:

.button {

  /* everything from before, plus... */

  &amp;::after {
    background: center / 24px 24px no-repeat; // Shorthand for: background-position, background-size, background-repeat
    border-radius: 100em;
    bottom: 0;
    content: '';
    position: absolute;
    right: 0;
    top: 0;
    width: 48px;
  }

  &amp;--download {

    &amp;::after {
      background-image: url( 'data:image/svg+xml;utf-8,' );
      }
    }

  &amp;--external {

    &amp;::after {
      background-image: url( 'data:image/svg+xml;utf-8,' );
    }
  }

  &amp;--caret-right {

    &amp;::after {
      background-image: url( 'data:image/svg+xml;utf-8,' );
    }
  }
}

Let’s pause here. While we’re keeping our SCSS as tidy as possible by declaring the properties common to all buttons, and then only specifying the background SVGs on a per-class basis, it’s already starting to look a bit unwieldy. That’s that second downside to SVGs I mentioned before: having to use big, ugly markup in our CSS code.

Also, note how we’re defining our fill and stroke colors inside the SVGs. At some point, browsers decided that the octothorpe (“#”) that we all know and love in our hex colors was a security risk, and declared that they would no longer support data URLs that contained them. This leaves us with three options:

  1. Convert our data URLs from markup (like we have here) to base-64 encoded strings, but that makes them even less maintainable than before by completely obfuscating them; or
  2. Use rgba() or hsla() notation, not always intuitive as many developers have been using hex for years; or
  3. Convert our octothorpes to their URL-encoded equivalents, “%23”.

We’re going to go with option number three, and work around that browser limitation. (I will mention here, however, that this technique will work with rgb(), hsla(), or any other valid color format, even CSS named colors. But please don’t use CSS named colors in production code.)

Moving to maps

At this point, we only have three buttons fully declared. But I don’t like them just dumped in the code like this. If we needed to use those same icons elsewhere, we’d have to copy and paste the SVG markup, or else we could assign them to variables (either Sass or CSS custom properties), and reuse them that way. But I’m going to go for what’s behind door number three, and switch to using one of Sass’ greatest features: maps.

If you’re not familiar with Sass maps, they are, in essence, the Sass version of an associative array. Instead of a numerically-indexed array of items, we can assign a name (a key, if you will) so that we can retrieve them by something logical and easily remembered. So let’s build a Sass map of our three icons:

$icons: (
  'download':    '',
  'external':    '',
  'caret-right': '',
);

There are two things to note here: We didn’t include the data:image/svg+xml;utf-8, string in any of those icons, only the SVG markup itself. That string is going to be the same every single time we need to use these icons, so why repeat ourselves and run the risk of making a mistake? Let’s instead define it as its own string and prepend it to the icon markup when needed:

$data-svg-prefix: 'data:image/svg+xml;utf-8,';

The other thing to note is that we aren’t actually making our SVG any prettier; there’s no way to do that. What we are doing is pulling all that ugliness out of the code we’re working on a day-to-day basis so we don’t have to look at all that visual clutter as much. Heck, we could even put it in its own partial that we only have to touch when we need to add more icons. Out of sight, out of mind!

So now, let’s use our map. Going back to our button code, we can now replace those icon literals with pulling them from the icon map instead:

&amp;--download {

  &amp;::after {
    background-image: url( $data-svg-prefix + map-get( $icons, 'download' ) );
  }
}

&amp;--external {

  &amp;::after {
    background-image: url( $data-svg-prefix + map-get( $icons, 'external' ) );
  }
}

&amp;--next {

  &amp;::after {
    background-image: url( $data-svg-prefix + map-get( $icons, 'caret-right' ) );
  }
}

Already, that’s looking much better. We’ve started abstracting out our icons in a way that keeps our code readable and maintainable. And if that were the only challenge, we’d be done. But in the real-world project that inspired this article, we had another wrinkle: different colors.

Our buttons are a solid color which turn to a darker version of that color on their hover state. But what if we want “ghost” buttons instead, that turn into solid colors on hover? In this case, white icons would be invisible for buttons that appear on white backgrounds (and probably look wrong on non-white backgrounds). What we’re going to need are two variations of each icon: the white one for the hover state, and one that matches button’s border and text color for the non-hover state.

Let’s update our button’s base CSS to turn it in from a solid button to a ghost button that turns solid on hover. And we’ll need to adjust the pseudo-elements for our icons, too, so we can swap them out on hover as well.

.button {
  appearance: none;
  background: none;
  border: 3px solid #d95a2b;
  border-radius: 100em;
  color: #d95a2b;
  cursor: pointer;
  display: inline-block;
  font-size: 18px;
  font-weight: bold;
  line-height: 1;
  padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
  position: relative;
  text-align: center;
  text-transform: uppercase;
  transition: 200ms ease-in-out;
  transition-property: background-color, color;

  &amp;:hover,
  &amp;:focus,
  &amp;:active {
    background: #d95a2b;
    color: #fff;
  }
}

Now we need to create our different-colored icons. One possible solution is to add the color variations directly to our map… somehow. We can either add new different-colored icons as additional items in our one-dimensional map, or make our map two-dimensional.

One-Dimensional Map:

$icons: (
  'download-white':  '',
  'download-orange': '',
);

Two-Dimensional Map:

$icons: (
  'download': (
    'white':  '',
    'orange': '',
  ),
);

Either way, this is problematic. By just adding one additional color, we’re going to double our maintenance efforts. Need to change the existing download icon with a different one? We need to manually create each color variation to add it to the map. Need a third color? Now you’ve just tripled your maintenance costs. I’m not even going to get into the code to retrieve values from a multi-dimensional Sass map because that’s not going to serve our ultimate goal here. Instead, we’re just going to move on.

Enter string replacement

Aside from maps, the utility of Sass in this article comes from how we can use it to make CSS behave more like a programming language. Sass has built-in functions (like map-get(), which we’ve already seen), and it allows us to write our own.

Sass also has a bunch of string functions built-in, but inexplicably, a string replacement function isn’t one of them. That’s too bad, as its usefulness is obvious. But all is not lost.

Kitty Giradel gave us a Sass version of str-replace() here on CSS-Tricks in 2014. We can use that here to create one version of our icons in our Sass map, using a placeholder for our color values. Let’s add that function to our own code:

@function str-replace( $string, $search, $replace: '' ) {

  $index: str-index( $string, $search );

  @if $index {
    @return str-slice( $string, 1, $index - 1 ) + $replace + str-replace( str-slice( $string, $index + str-length( $search ) ), $search, $replace);
  }

  @return $string;
}

Next, we’ll update our original Sass icon map (the one with only the white versions of our icons) to replace the white with a placeholder (%%COLOR%%) that we can swap out with whatever color we call for, on demand.

$icons: (
  'download':    '',
  'external':    '',
  'caret-right': '',
);

But if we were going to try and fetch these icons using just our new str-replace() function and Sass’ built-in map-get() function, we’d end with something big and ugly. I’d rather tie these two together with one more function that makes calling the icon we want in the color we want as simple as one function with two parameters (and because I’m particularly lazy, we’ll even make the color default to white, so we can omit that parameter if that’s the color icon we want).

Because we’re getting an icon, it’s a “getter” function, and so we’ll call it get-icon():

@function get-icon( $icon, $color: #fff ) {

  $icon:        map-get( $icons, $icon );
  $placeholder: '%%COLOR%%';

  $data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );

  @return str-replace( $data-uri, '#', '%23' );
}

Remember where we said that browsers won’t render data URLs that have octothorpes in them? Yeah, we’re str-replace()ing that too so we don’t have to remember to pass along “%23” in our color hex codes.

Side note: I have a Sass function for abstracting colors too, but since that’s outside the scope of this article, I’ll refer you to my get-color() gist to peruse at your leisure.

The result

Now that we have our get-icon() function, let’s put it to use. Going back to our button code, we can replace our map-get() function with our new icon getter:

&amp;--download {

  &amp;::before {
    background-image: get-icon( 'download', #d95a2b );
  }

  &amp;::after {
    background-image: get-icon( 'download', #fff ); // The ", #fff" isn't strictly necessary, because white is already our default
  }
}

&amp;--external {

  &amp;::before {
    background-image: get-icon( 'external', #d95a2b );
  }

  &amp;::after {
    background-image: get-icon( 'external' );
  }
}

&amp;--next {

  &amp;::before {
    background-image: get-icon( 'arrow-right', #d95a2b );
  }

  &amp;::after {
    background-image: get-icon( 'arrow-right' );
  }
}

So much easier, isn’t it? Now we can call any icon we’ve defined, with any color we need. All with simple, clean, logical code.

  • We only ever have to declare an SVG in one place.
  • We have a function that gets that icon in whatever color we give it.
  • Everything is abstracted out to a logical function that does exactly what it looks like it will do: get X icon in Y color.

Making it fool-proof

The one thing we’re lacking is error-checking. I’m a huge believer in failing silently… or at the very least, failing in a way that is invisible to the user yet clearly tells the developer what is wrong and how to fix it. (For that reason, I should be using unit tests way more than I do, but that’s a topic for another day.)

One way we have already reduced our function’s propensity for errors is by setting a default color (in this case, white). So if the developer using get-icon() forgets to add a color, no worries; the icon will be white, and if that’s not what the developer wanted, it’s obvious and easily fixed.

But wait, what if that second parameter isn’t a color? As if, the developer entered a color incorrectly, so that it was no longer being recognized as a color by the Sass processor?

Fortunately we can check for what type of value the $color variable is:

@function get-icon( $icon, $color: #fff ) {

  @if 'color' != type-of( $color ) {

    @warn 'The requested color - "' + $color + '" - was not recognized as a Sass color value.';
    @return null;
  }

  $icon:        map-get( $icons, $icon );
  $placeholder: '%%COLOR%%';
  $data-uri:    str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );

  @return str-replace( $data-uri, '#', '%23' );
}

Now if we tried to enter a nonsensical color value:

&amp;--download {

  &amp;::before {
    background-image: get-icon( 'download', ce-nest-pas-un-couleur );
  }
}

…we get output explaining our error:

Line 25 CSS: The requested color - "ce-nest-pas-un-couleur" - was not recognized as a Sass color value.

…and the processing stops.

But what if the developer doesn’t declare the icon? Or, more likely, declares an icon that doesn’t exist in the Sass map? Serving a default icon doesn’t really make sense in this scenario, which is why the icon is a mandatory parameter in the first place. But just to make sure we are calling an icon, and it is valid, we’re going to add another check:

@function get-icon( $icon, $color: #fff ) {

  @if 'color' != type-of( $color ) {

    @warn 'The requested color - "' + $color + '" - was not recognized as a Sass color value.';
    @return null;
  }

  @if map-has-key( $icons, $icon ) {

    $icon:        map-get( $icons, $icon );
    $placeholder: '%%COLOR%%';
    $data-uri:    str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );

    @return str-replace( $data-uri, '#', '%23' );
  }

  @warn 'The requested icon - "' + $icon + '" - is not defined in the $icons map.';
  @return null;
}

We’ve wrapped the meat of the function inside an @if statement that checks if the map has the key provided. If so (which is the situation we’re hoping for), the processed data URL is returned. The function stops right then and there — at the @return — which is why we don’t need an @else statement.

But if our icon isn’t found, then null is returned, along with a @warning in the console output identifying the problem request, plus the partial filename and line number. Now we know exactly what’s wrong, and when and what needs fixing.

So if we were to accidentally enter:

&amp;--download {

  &amp;::before {
    background-image: get-icon( 'ce-nest-pas-une-icône', #d95a2b );
  }
}

…we would see the output in our console, where our Sass process was watching and running:

Line 32 CSS: The requested icon - "ce-nest-pas-une-icône" - is not defined in the $icons map.

As for the button itself, the area where the icon would be will be blank. Not as good as having our desired icon there, but soooo much better than a broken image graphic or some such.

Conclusion

After all of that, let’s take a look at our final, processed CSS:

.button {
  -webkit-appearance: none;
      -moz-appearance: none;
          appearance: none;
  background: none;
  border: 3px solid #d95a2b;
  border-radius: 100em;
  color: #d95a2b;
  cursor: pointer;
  display: inline-block;
  font-size: 18px;
  font-weight: 700;
  line-height: 1;
  padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;
  position: relative;
  text-align: center;
  text-transform: uppercase;
  transition: 200ms ease-in-out;
  transition-property: background-color, color;
}
.button:hover, .button:active, .button:focus {
  background: #d95a2b;
  color: #fff;
}
.button::before, .button::after {
  background: center / 24px 24px no-repeat;
  border-radius: 100em;
  bottom: 0;
  content: '';
  position: absolute;
  right: 0;
  top: 0;
  width: 48px;
}
.button::after {
  opacity: 0;
  transition: opacity 200ms ease-in-out;
}
.button:hover::after, .button:focus::after, .button:active::after {
  opacity: 1;
}
.button--download::before {
  background-image: url('data:image/svg+xml;utf-8,');
}
.button--download::after {
  background-image: url('data:image/svg+xml;utf-8,');
}
.button--external::before {
  background-image: url('data:image/svg+xml;utf-8,');
}
.button--external::after {
  background-image: url('data:image/svg+xml;utf-8,');
}
.button--next::before {
  background-image: url('data:image/svg+xml;utf-8,');
}
.button--next::after {
  background-image: url('data:image/svg+xml;utf-8,');
}

Yikes, still ugly, but it’s ugliness that becomes the browser’s problem, not ours.

I’ve put all this together in CodePen for you to fork and experiment. The long goal for this mini-project is to create a PostCSS plugin to do all of this. This would increase the availability of this technique to everyone regardless of whether they were using a CSS preprocessor or not, or which preprocessor they’re using.

“If I have seen further it is by standing on the shoulders of Giants.”
– Isaac Newton, 1675

Of course we can’t talk about Sass and string replacement and (especially) SVGs without gratefully acknowledging the contributions of the others who’ve inspired this technique.


Creating a Maintainable Icon System with Sass originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-a-maintainable-icon-system-with-sass/feed/ 10 294949