{"id":294949,"date":"2019-08-28T07:05:56","date_gmt":"2019-08-28T14:05:56","guid":{"rendered":"https:\/\/css-tricks.com\/?p=294949"},"modified":"2021-08-03T12:49:49","modified_gmt":"2021-08-03T19:49:49","slug":"creating-a-maintainable-icon-system-with-sass","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/creating-a-maintainable-icon-system-with-sass\/","title":{"rendered":"Creating a Maintainable Icon System with Sass"},"content":{"rendered":"
One of my favorite ways of adding icons to a site is by including them as data URL<\/a> background images to pseudo-elements (e.g. But there are some drawbacks to this technique as well:<\/p>\n We’re going to address both of these drawbacks in this article.<\/p>\n <\/p>\n 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:<\/p>\n 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.<\/p>\n 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<\/a> to deal with things like the 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:<\/p>\n Let’s pause here. While we’re keeping our SCSS as tidy as possible by declaring the properties common to all<\/em> 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.<\/p>\n 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<\/a>. This leaves us with three options:<\/p>\n We’re going to go with option number three, and work around that browser limitation. (I will mention here, however, that this technique will<\/em> work with 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<\/a>, and switch to using one of Sass’ greatest features: maps.<\/p>\n If you’re not familiar with Sass maps<\/a>, 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:<\/p>\n There are two things to note here: We didn’t include the The other thing to note is that we aren’t actually<\/em> making our SVG any prettier; there’s no way to do that. What we are<\/em> 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!<\/p>\n 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:<\/p>\n 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.<\/p>\n 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<\/em> 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.<\/p>\n 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.<\/p>\n Now we need to create our different-colored icons. One possible<\/em> 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.<\/p>\n One-Dimensional Map:<\/strong><\/p>\n Two-Dimensional Map:<\/strong><\/p>\n 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<\/em> 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.<\/p>\n 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 Sass also has a bunch of string functions built-in<\/a>, 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.<\/p>\n Kitty Giradel gave us a Sass version of Next, we’ll update our original<\/em> Sass icon map (the one with only the white versions of our icons) to replace the white with a placeholder ( But if we were going to try and fetch these icons using just our new Because we’re getting<\/em> an icon, it’s a “getter” function, and so we’ll call it Remember where we said that browsers won’t render data URLs that have octothorpes in them? Yeah, we’re Side note: I have a Sass function for abstracting colors<\/a> too, but since that’s outside the scope of this article, I’ll refer you to my Now that we have our 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.<\/p>\n The one thing we’re lacking is error-checking. I’m a huge<\/em> 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<\/em> be using unit tests way more than I do, but that’s a topic for another day.)<\/p>\n 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 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<\/em> a color by the Sass processor?<\/p>\n Fortunately we can check for what type of value the Now if we tried to enter a nonsensical color value:<\/p>\n …we get output explaining our error:<\/p>\n …and the processing stops.<\/p>\n 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<\/em> 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<\/em> we are calling an icon, and it is valid, we’re going to add another check:<\/p>\n We’ve wrapped the meat of the function inside an But if our icon isn’t<\/em> found, then So if we were to accidentally enter:<\/p>\n …we would see the output in our console, where our Sass process was watching and running:<\/p>\n 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<\/em> much better than a broken image graphic or some such.<\/p>\n After all of that, let’s take a look at our final, processed<\/em> CSS:<\/p>\n Yikes, still ugly, but it’s ugliness that becomes the browser’s problem, not ours.<\/p>\n I’ve put all this together in CodePen for you to fork and experiment<\/a>. 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<\/em> regardless of whether they were using a CSS preprocessor or not, or which<\/em> preprocessor they’re using.<\/p>\n “If I have seen further it is by standing on the shoulders of Giants.” 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.<\/p>\n 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 […]<\/p>\n","protected":false},"author":264753,"featured_media":283681,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_bbp_topic_count":0,"_bbp_reply_count":0,"_bbp_total_topic_count":0,"_bbp_total_reply_count":0,"_bbp_voice_count":0,"_bbp_anonymous_reply_count":0,"_bbp_topic_count_hidden":0,"_bbp_reply_count_hidden":0,"_bbp_forum_subforum_count":0,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"c2c_always_allow_admin_comments":false,"footnotes":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":[]},"categories":[4],"tags":[1220,626,476,1001,660],"jetpack_publicize_connections":[],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2019\/02\/iconsvg-xyz.png?fit=1200%2C600&ssl=1","jetpack-related-posts":[],"featured_media_src_url":"https:\/\/i0.wp.com\/css-tricks.com\/wp-content\/uploads\/2019\/02\/iconsvg-xyz.png?fit=1024%2C512&ssl=1","_links":{"self":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/294949"}],"collection":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/users\/264753"}],"replies":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/comments?post=294949"}],"version-history":[{"count":10,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/294949\/revisions"}],"predecessor-version":[{"id":345962,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/posts\/294949\/revisions\/345962"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media\/283681"}],"wp:attachment":[{"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/media?parent=294949"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/categories?post=294949"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/css-tricks.com\/wp-json\/wp\/v2\/tags?post=294949"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}::after<\/code>) in my CSS. This technique offers several advantages:<\/p>\n
\n
background-size<\/code> property, you can set your pseudo-element to any size you need without worrying that they will overflow the boundaries (or get chopped off).<\/li>\n
\n
background-image<\/code> 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' )<\/code>). We can use
filter()<\/code> as an alternative<\/a>, but that might not always be a feasible solution.<\/li>\n
The situation<\/h3>\n
\n
appearance<\/code> property.)<\/p>\n
.button {\n appearance: none;\n background: #d95a2b;\n border: 0;\n border-radius: 100em;\n color: #fff;\n cursor: pointer;\n display: inline-block;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;\n position: relative;\n text-align: center;\n text-transform: uppercase;\n transition: background-color 200ms ease-in-out;\n\n &:hover,\n &:focus,\n &:active {\n background: #8c3c2a;\n }\n}<\/code><\/pre>\n
.button {\n\n \/* everything from before, plus... *\/\n\n &::after {\n background: center \/ 24px 24px no-repeat; \/\/ Shorthand for: background-position, background-size, background-repeat\n border-radius: 100em;\n bottom: 0;\n content: '';\n position: absolute;\n right: 0;\n top: 0;\n width: 48px;\n }\n\n &--download {\n\n &::after {\n background-image: url( 'data:image\/svg+xml;utf-8,' );\n }\n }\n\n &--external {\n\n &::after {\n background-image: url( 'data:image\/svg+xml;utf-8,' );\n }\n }\n\n &--caret-right {\n\n &::after {\n background-image: url( 'data:image\/svg+xml;utf-8,' );\n }\n }\n}<\/code><\/pre>\n
\n
rgba()<\/code> or
hsla()<\/code> notation, not always intuitive as many developers have been using hex for years; or<\/li>\n
rgb()<\/code>,
hsla()<\/code>, or any other valid color format, even CSS named colors<\/a>. But please don’t use CSS named colors in production code.)<\/p>\n
Moving to maps<\/h3>\n
$icons: (\n 'download': '',\n 'external': '',\n 'caret-right': '',\n);<\/code><\/pre>\n
data:image\/svg+xml;utf-8,<\/code> string in any of those icons, only the SVG markup itself. That string is going to be the same every single time<\/em> 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:<\/p>\n
$data-svg-prefix: 'data:image\/svg+xml;utf-8,';<\/code><\/pre>\n
&--download {\n\n &::after {\n background-image: url( $data-svg-prefix + map-get( $icons, 'download' ) );\n }\n}\n\n&--external {\n\n &::after {\n background-image: url( $data-svg-prefix + map-get( $icons, 'external' ) );\n }\n}\n\n&--next {\n\n &::after {\n background-image: url( $data-svg-prefix + map-get( $icons, 'caret-right' ) );\n }\n}<\/code><\/pre>\n
.button {\n appearance: none;\n background: none;\n border: 3px solid #d95a2b;\n border-radius: 100em;\n color: #d95a2b;\n cursor: pointer;\n display: inline-block;\n font-size: 18px;\n font-weight: bold;\n line-height: 1;\n padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;\n position: relative;\n text-align: center;\n text-transform: uppercase;\n transition: 200ms ease-in-out;\n transition-property: background-color, color;\n\n &:hover,\n &:focus,\n &:active {\n background: #d95a2b;\n color: #fff;\n }\n}<\/code><\/pre>\n
$icons: (\n 'download-white': '',\n 'download-orange': '',\n);<\/code><\/pre>\n
$icons: (\n 'download': (\n 'white': '',\n 'orange': '',\n ),\n);<\/code><\/pre>\n
Enter string replacement<\/h3>\n
map-get()<\/code>, which we’ve already seen), and it allows us to write our own.<\/p>\n
str-replace()<\/code> here on CSS-Tricks in 2014.<\/a> We can use that here to create one<\/em> version of our icons in our Sass map, using a placeholder for our color values. Let’s add that function to our own code:<\/p>\n
@function str-replace( $string, $search, $replace: '' ) {\n\n $index: str-index( $string, $search );\n\n @if $index {\n @return str-slice( $string, 1, $index - 1 ) + $replace + str-replace( str-slice( $string, $index + str-length( $search ) ), $search, $replace);\n }\n\n @return $string;\n}<\/code><\/pre>\n
%%COLOR%%<\/code>) that we can swap out with whatever color we call for, on demand.<\/p>\n
$icons: (\n 'download': '',\n 'external': '',\n 'caret-right': '',\n);<\/code><\/pre>\n
str-replace()<\/code> function and Sass’ built-in
map-get()<\/code> 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<\/em> 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).<\/p>\n
get-icon()<\/code>:<\/p>\n
@function get-icon( $icon, $color: #fff ) {\n\n $icon: map-get( $icons, $icon );\n $placeholder: '%%COLOR%%';\n\n $data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );\n\n @return str-replace( $data-uri, '#', '%23' );\n}<\/code><\/pre>\n
str-replace()<\/code>ing that too so we don’t have to remember to pass along “%23” in our color hex codes.<\/p>\n
get-color()<\/code> gist to peruse at your leisure.<\/p>\n
The result<\/h3>\n
get-icon()<\/code> function, let’s put it to use. Going back to our button code, we can replace our
map-get()<\/code> function with our new icon getter:<\/p>\n
&--download {\n\n &::before {\n background-image: get-icon( 'download', #d95a2b );\n }\n\n &::after {\n background-image: get-icon( 'download', #fff ); \/\/ The \", #fff\" isn't strictly necessary, because white is already our default\n }\n}\n\n&--external {\n\n &::before {\n background-image: get-icon( 'external', #d95a2b );\n }\n\n &::after {\n background-image: get-icon( 'external' );\n }\n}\n\n&--next {\n\n &::before {\n background-image: get-icon( 'arrow-right', #d95a2b );\n }\n\n &::after {\n background-image: get-icon( 'arrow-right' );\n }\n}<\/code><\/pre>\n
\n
Making it fool-proof<\/h3>\n
get-icon()<\/code> 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.<\/p>\n
$color<\/code> variable is:<\/p>\n
@function get-icon( $icon, $color: #fff ) {\n\n @if 'color' != type-of( $color ) {\n\n @warn 'The requested color - \"' + $color + '\" - was not recognized as a Sass color value.';\n @return null;\n }\n\n $icon: map-get( $icons, $icon );\n $placeholder: '%%COLOR%%';\n $data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );\n\n @return str-replace( $data-uri, '#', '%23' );\n}<\/code><\/pre>\n
&--download {\n\n &::before {\n background-image: get-icon( 'download', ce-nest-pas-un-couleur );\n }\n}<\/code><\/pre>\n
Line 25 CSS: The requested color - \"ce-nest-pas-un-couleur\" - was not recognized as a Sass color value.<\/code><\/pre>\n
@function get-icon( $icon, $color: #fff ) {\n\n @if 'color' != type-of( $color ) {\n\n @warn 'The requested color - \"' + $color + '\" - was not recognized as a Sass color value.';\n @return null;\n }\n\n @if map-has-key( $icons, $icon ) {\n\n $icon: map-get( $icons, $icon );\n $placeholder: '%%COLOR%%';\n $data-uri: str-replace( url( $data-svg-prefix + $icon ), $placeholder, $color );\n\n @return str-replace( $data-uri, '#', '%23' );\n }\n\n @warn 'The requested icon - \"' + $icon + '\" - is not defined in the $icons map.';\n @return null;\n}<\/code><\/pre>\n
@if<\/code> 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 \u2014 at the
@return<\/code> \u2014 which is why we don’t need an
@else<\/code> statement.<\/p>\n
null<\/code> is returned, along with a
@warn<\/code>ing in the console output identifying the problem request, plus the partial filename and line number. Now we know exactly<\/em> what’s wrong, and when and what needs fixing.<\/p>\n
&--download {\n\n &::before {\n background-image: get-icon( 'ce-nest-pas-une-ic\u00f4ne', #d95a2b );\n }\n}<\/code><\/pre>\n
Line 32 CSS: The requested icon - \"ce-nest-pas-une-ic\u00f4ne\" - is not defined in the $icons map.<\/code><\/pre>\n
Conclusion<\/h3>\n
.button {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n background: none;\n border: 3px solid #d95a2b;\n border-radius: 100em;\n color: #d95a2b;\n cursor: pointer;\n display: inline-block;\n font-size: 18px;\n font-weight: 700;\n line-height: 1;\n padding: 1em calc( 1.5em + 32px ) 0.9em 1.5em;\n position: relative;\n text-align: center;\n text-transform: uppercase;\n transition: 200ms ease-in-out;\n transition-property: background-color, color;\n}\n.button:hover, .button:active, .button:focus {\n background: #d95a2b;\n color: #fff;\n}\n.button::before, .button::after {\n background: center \/ 24px 24px no-repeat;\n border-radius: 100em;\n bottom: 0;\n content: '';\n position: absolute;\n right: 0;\n top: 0;\n width: 48px;\n}\n.button::after {\n opacity: 0;\n transition: opacity 200ms ease-in-out;\n}\n.button:hover::after, .button:focus::after, .button:active::after {\n opacity: 1;\n}\n.button--download::before {\n background-image: url('data:image\/svg+xml;utf-8,');\n}\n.button--download::after {\n background-image: url('data:image\/svg+xml;utf-8,');\n}\n.button--external::before {\n background-image: url('data:image\/svg+xml;utf-8,');\n}\n.button--external::after {\n background-image: url('data:image\/svg+xml;utf-8,');\n}\n.button--next::before {\n background-image: url('data:image\/svg+xml;utf-8,');\n}\n.button--next::after {\n background-image: url('data:image\/svg+xml;utf-8,');\n}<\/code><\/pre>\n
\n\u2013 Isaac Newton, 1675<\/small><\/p><\/blockquote>\n\n