Fun with Sass & font icons

⚠️ This article is outdated: CSS in JS is far superior to Sass, and font icons are no longer a good idea. Use inline SVG React components and in certain situations plain old .svg files in img tags.

Icon fonts have been been best-practice for a while now. They allow us to use tons of fully styleable cross-browser vector icons with one lightweight HTTP request.

Folk typically use font icons via non-semantic presentational markup such as class="icon icon-happy-face" that should be avoided. With a pinch of Sass you can add icons to elements purely via your stylesheet using easy to remember names, without polluting your markup. Yay!

Setup

1. Prepare the font

These are some of the better options:

  • Font Custom to manage icons as SVGs and generate fonts locally.
  • IcoMoon to pick from a wide range of icons and generate fonts online.

By far the most intelligent workflow is a clever Font Custom setup automating the font generation and all this Sass config, using IcoMoon as a handy source for SVG icons. This article however is agnostic to how you source your icon font.

2. Declare the font

You can use a boring old @font-face declaration, but here I use the nifty Bourbon mixin:

@include font-face("Icons", "../fonts/icons/icons");

3. List the icons

Next specify your icon names along with their matching unicode in the font:

// Map icon names to font unicode characters
$icons: (
  email: "\f000",
  newspaper: "\f001",
  info: "\f002",
  people: "\f003",
  arrow-left: "\f004",
  arrow-right: "\f005"
);

Manually setting up the characters sucks, I know. That clever Font Custom setup I was talking about handles this 100%.

4. Add the mixin

Lastly include the multi-purpose icon Sass mixin for use throughout your styles:

// For adding font icons to elements using CSS pseudo-elements
// http://jaydenseric.com/blog/fun-with-sass-and-font-icons
@mixin icon($position: before, $icon: false, $styles: true) {
  @if $position == both {
    $position: "before, &:after";
  }
  // Either a :before or :after pseudo-element, or both, defaulting to :before
  &:#{$position} {
    @if $icon {
      // A particular icon has been specified
      content: "#{map-get($icons, $icon)}";
    }
    @if $styles {
      // Supportive icon styles required
      speak: none;
      font-style: normal;
      font-weight: normal;
      font-family: "Icons";
    }
    // Include any extra rules supplied for the pseudo-element
    @content;
  }
}

Usage

The icon($position: before, $icon: false, $styles: true) mixin can help you:

  1. Attach a particular icon to an element complete with all the required styles.
  2. Attach just the required icon styles to a bunch of elements, without setting a particular icon.
  3. Set or swap the icon on an element without applying required styles.
  4. Set custom styles for your icon pseudo-element.

Attach a complete icon

With the following element:

<a href="mailto:foo@bar.com">Email me</a>

Easily add an icon with all the required styles:

[href^="mailto"] {
  @include icon(before, email);
}

Which compiles to the following CSS:

[href^="mailto"]:before {
  content: "\f000";
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-family: "Icons";
}

Customize icons

You can apply custom styles to an icon pseudo-element by using brackets on the mixin. With the previous example, adding a few styles to an icon is easy:

[href^="mailto"] {
  @include icon(before, email) {
    margin-right: 20px;
    color: blue;
  }
}

This compiles to the following CSS:

[href^="mailto"]:before {
  content: "\f000";
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-family: "Icons";
  margin-right: 20px;
  color: blue;
}

Icons before & after

This should not come up often. Here’s the convenient way to handle it:

<button class="expand">Expand Horizontally</button>
.expand {
  @include icon(both) {
    color: gray;
  }
  @include icon(before, arrow-left, false) {
    margin-right: 10px;
  }
  @include icon(after, arrow-right, false) {
    margin-left: 10px;
  }
}

This compiles to the following CSS:

.expand:before,
.expand:after {
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-family: "Icons";
  color: gray:
}
.expand:before {
  content: "\f004";
  margin-right: 10px;
}
.expand:after {
  content: "\f005";
  margin-left: 10px;
}

Attach an icon series

With the following elements:

<nav>
  <a href="/news">News</a>
  <a href="/about">About us</a>
  <a href="/contact">Get in touch</a>
</nav>

Add just the required styles to the all items, then specify each icon:

nav {
  a {
    @include icon;
  }
  [href*="news"] {
    @include icon(before, newspaper, false);
  }
  [href*="about"] {
    @include icon(before, info, false);
  }
  [href*="contact"] {
    @include icon(before, people, false);
  }
}

Which compiles to the following CSS:

nav a:before {
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-family: "Icons";
}
nav [href*="news"]:before {
  content: "\f001";
}
nag [href*="about"]:before {
  content: "\f002";
}
nav [href*="contact"]:before {
  content: "\f003";
}

Setup icon classnames

If after all the above you still want to use nasty class names to add icons to elements, you can now set them up automagically:

// Set the required styles on all icons
[class^="icon-"],
[class*=" icon-"] {
  @include icon;
}

// Setup a class name for each icon
@each $name, $char in $icons {
  .icon-#{$name} {
    content: $char;
  }
}

This will output:

[class^="icon-"]:before,
[class*=" icon-"]:before {
  speak: none;
  font-style: normal;
  font-weight: normal;
  font-family: "Icons";
}
.icon-email:before {
  content: "\f000";
}
.icon-newspaper:before {
  content: "\f001";
}
.icon-info:before {
  content: "\f002";
}
.icon-people:before {
  content: "\f003";
}
.icon-arrow-left:before {
  content: "\f004";
}
.icon-arrow-right:before {
  content: "\f005";
}