Ligature Icons

Icon fonts have fallen out of favor in recent years because SVG support has gotten good enough to be viewable by virtually everyone using a browser. There’s a good reason for the switch to SVG other than that, of course. Icon fonts are generally used in the conventional manner of assigning an icon to a character in a font, using the character in the markup, and then styling the containing element to use the icon font. It never should have been considered a viable method of displaying icons on a webpage. It littered the markup with stray characters that most of the time bore very little semantic meaning to the icons that would be displayed in an ideal setting. It also was an issue when screen readers were involved, creating odd — and sometimes comical — interjections of random characters that were read out instead of an icon. A simple img element would be preferable to that.

SVG icons work really well today as long as you embed the SVG into the document. If sourcing a SVG through an img element by its identifier various problems begin occurring in different browsers — except Firefox which handles everything you throw at it. Embedding SVG into every document isn’t efficient and isn’t friendly to the browser’s cache either. If there are only a few icons this is okay, but for several it’s not a good option at all. What if there were a way to use icon fonts and have them be both semantic and degrade gracefully? There is, and I’ve used them on this website for almost five years. I only know of one write-up about ligature icons which is from CSS Tricks a couple of years ago about how Google’s Material Design icons were ligature icons. It kind of fell flat because it didn’t go into any detail of how to make them. I’ll outline a way to do so here.


Ampersand typeset in Bodoni Poster Condensed, eszett in Tungsten Black, oe in Alter Gotisch, AE in Akzidenz Grotesk Extended Bold, and ffi in Adobe Jenson Pro

I hope all designers are aware of ligatures. Some people reading this might not be, so I’ll explain a little bit. They’re combinations of letters into a single glyph like the ones displayed above. The most well known of the bunch of course is the ampersand which originated as a ligature of “et” which is Latin for “and”. Aside from the ampersand and other characters like the German ß that became so commonplace they just became yet another symbol used, they haven’t been available for use on the Web except when someone has discovered the wonders of Unicode and decided to paste in Unicode representations of them like many do on their Twitter profiles. Automatic insertion of ligatures is supported by a vast majority of browsers now and have been for some time.

This is interesting and all, but how are developers and designers going to create icon fonts? Well, the usual approach to this would be to use specialized font software to create them such as Fontlab or FontForge. In fact software like that is how I created my font originally, but I discovered another way that doesn’t require much in the way of super specialized software like a FontLab or FontForge.

Math?! Ew!

Before getting into how to create them I should explain a bit about how fonts work internally. Internally fonts use a measurement called UPM which is short for units per em. In metal type it was the size of the box the letter rose out of, and in digital type it’s the resolution of the font itself. Most font formats cannot use floating point numbers in the positions of points, so a font with 1000 UPM has 1000 units2 to use within that box, but unlike metal type digital type can excede the box. I used 1000 UPM in my icon font. 2048 is a common UPM size; it provides more resolution for the font, and it’s common because in the old days where processing power and memory wasn’t remotely as abundant as it is today it was convenient to have a nice, round number in binary. Today we can use whatever we want. Actually, it might even make sense to use something which is proportional to the size of your icons; for instance if your icons are 24×24 then the UPM would be 2400. I haven’t personally experimented with this yet, but it makes sense.

What? SVG?

Indeed. You can author fonts in SVG 1.1. They have been removed in SVG 2.0 which I see as a shortsighted mistake; that’s for another time, but what I’ll show here says a bit of why it’s a mistake to deprecate SVG fonts. Chrome and Firefox have removed support for SVG fonts, so why bother with them? It’s what the icon font will be authored as, not what it will be delivered as. At the end of this process a TrueType font will be generated.

<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
  <font id="dWIcons" horiz-adv-x="0">
   <font-face font-family="dW Icons" font-weight="normal" font-style="normal" units-per-em="1000" ascent="500" descent="-500">
     <font-face-name name="dW Icons Regular"/>
   <glyph unicode="ArtStation" glyph-name="artstation" horiz-adv-x="1000" d="M0,12,84-135c18-32,50-55,91-55H735L619,12Zm1000-2c0,21-7,39-16,55L655,636c-17,32-50,54-89,54H392L900-189,980-51c16,27,20,39,20,61ZM536,155,309,548,83,155Z"/>
   <glyph unicode="Dribbble" glyph-name="dribbble" horiz-adv-x="1000" d="M118-71C41,20,0,133,0,253H4c62,0,279,5,504,71l31-64C301,186,154-12,118-71ZM577,464C506,592,431,699,401,740a552.375,552.375,0,0,0,99,9c119,0,233-41,323-118-23-28-97-109-246-167ZM14,367C48,510,146,632,278,698c26-36,102-145,175-273C251,374,74,368,14,367Zm986-97c-75,14-150,22-223,22-38,0-76-3-113-7-17,39-26,60-36,80,156,67,241,155,269,189a497.519,497.519,0,0,0,103-284ZM795-153c-9,52-36,180-91,333a769.388,769.388,0,0,0,95,6,724.955,724.955,0,0,0,193-26A500.972,500.972,0,0,0,795-153ZM500-250c-109,0-212,35-299,100,24,44,129,210,383,301,64-169,93-312,102-365q-88.5-36-186-36Z"/>
   <glyph unicode="Email" glyph-name="email" horiz-adv-x="1000" d="M917,583H83A82.8,82.8,0,0,1,0,500V0A82.8,82.8,0,0,1,83-83H917a82.8,82.8,0,0,1,83,83V500a82.8,82.8,0,0,1-83,83ZM605,250C747,144,852,69,958-3a41.3,41.3,0,0,0-41-38H884C794,30,703,102,558,215c-32-22-41-26-58-26s-27,4-58,26C297,103,207,31,116-41H83A41.3,41.3,0,0,0,42-3C148,68,252,145,394,250,252,355,148,432,42,503c2,21,20,38,41,38h33c91-72,182-146,273-215,91-70,103-81,110-81s20,11,112,81c93,71,182,144,273,215h33c21,0,39-17,41-38C852,431,747,356,645,279l-40-29Z"/>
   <glyph unicode="Feeds" glyph-name="feeds" horiz-adv-x="1000" d="M214.5,750h570a214.67,214.67,0,0,0,215-215V-35a214.67,214.67,0,0,0-215-215h-570A214.67,214.67,0,0,0-.5-35V535a214.67,214.67,0,0,0,215,215Zm-84-768c0-58,47-106,105-106a106.053,106.053,0,0,1,106,106c0,58-47,105-106,105a104.987,104.987,0,0,1-105-105Zm0,404V237c199,0,360-162,360-361h150c0,282-229,510-510,510Zm744-511c0,414-336,750-750,750V475c331,0,600-269,600-600Z"/>
   <glyph unicode="GitHub" glyph-name="github" horiz-adv-x="1000" d="M500,738C224,738,0,514,0,238,0,17,143-170,342-237a22.018,22.018,0,0,1,8-1c19,0,26,13,26,26,0,11,0,43-1,85q-28.5-6-51-6c-96,0-117,73-117,73-23,57-56,73-56,73-14,10-19,16-19,21,0,9,22,9,23,9,50-3,76-51,76-51,25-43,59-55,89-55,23,0,44,7,57,13,4,32,17,54,32,67C298,30,181,72,181,264c0,55,19,99,51,134-3,7-10,28-10,57,0,12,2,77,25,77,18,0,59-7,128-53a471.359,471.359,0,0,0,125,17,477.028,477.028,0,0,0,125-17c68,46,110,53,127,53,24,0,26-65,26-76,0-30-8-51-11-58,33-35,52-79,52-134C819,72,702,30,591,17c18-15,34-46,34-92,0-67-1-121-1-137,0-12,7-25,25-25,2,0,6,0,9,1,199,66,342,253,342,474,0,276-224,500-500,500Z"/>
   <glyph unicode="Last.fm" glyph-name="lastfm" horiz-adv-x="1000" d="M.5,240c0,182,90,288,257,288,156,0,227-55,277-204l38-115c30-92,84-144,192-144,80,0,120,19,120,59,0,33-26,59-79,71l-78,18c-88,20-132,71-132,151,0,125,99,164,203,164,122,0,188-49,198-146l-116-14c-4,47-33,70-88,70-51,0-82-22-82-61,0-36,17-56,66-67l73-16c100-22,151-77,151-164,0-105-80-158-238-158-184,0-264,73-304,200l-37,116c-31,98-69,148-160,148-79,0-140-61-140-191,0-109,56-179,134-179,52,1,110,27,149,66l37-100c-40-29-117-60-191-60C89.5-28,.5,67,.5,240Z"/>
   <glyph unicode="Mastodon" glyph-name="mastodon" horiz-adv-x="1000" d="M940,151C926,80,819,3,696-12c-54-7-107-13-162-13C409-25,304,4,304,3c0-146,113-152,212-152,84,0,155,23,155,23l4-83s-74-41-206-41a731.4,731.4,0,0,0-194,26C54-166,43,100,43,341v81c0,217,140,280,140,280,70,33,191,47,317,48h3c125-1,246-15,316-48,4,0,140-65,140-292,0-38-1-166-19-259ZM792,408c0,105-56,175-148,175-52,0-92-20-118-61l-25-44-25,44c-26,41-66,61-118,61-92,0-148-70-148-175V145H311V400q0,81,66,81c49,0,74-32,74-96V245H551V385c0,64,25,96,74,96q66,0,66-81V145H792V408Z"/>
   <glyph unicode="Twitter" glyph-name="twitter" horiz-adv-x="1000" d="M314-156c389,0,595,328,584,610,40,29,74,65,102,106a420,420,0,0,0-118-32c42,25,75,65,90,113-39-23-83-40-130-50-37,40-91,65-150,65-113,0-205-92-205-205a241.626,241.626,0,0,1,5-47c-170,9-321,91-422,215A204.013,204.013,0,0,1,42,516c0-72,36-134,91-171a207.8,207.8,0,0,0-93,25c-1-101,70-184,165-203a225.516,225.516,0,0,0-93-4C138,82,214,22,304,21,233-34,145-67,49-67A390.782,390.782,0,0,0,0-64c91-58,199-92,314-92Z"/>
   <glyph unicode="." glyph-name="period"/>
   <glyph unicode="A" glyph-name="A"/>
   <glyph unicode="D" glyph-name="D"/>
   <glyph unicode="E" glyph-name="E"/>
   <glyph unicode="F" glyph-name="F"/>
   <glyph unicode="G" glyph-name="G"/>
   <glyph unicode="L" glyph-name="L"/>
   <glyph unicode="M" glyph-name="M"/>
   <glyph unicode="S" glyph-name="T"/>
   <glyph unicode="T" glyph-name="T"/>
   <glyph unicode="a" glyph-name="a"/>
   <glyph unicode="b" glyph-name="b"/>
   <glyph unicode="d" glyph-name="d"/>
   <glyph unicode="e" glyph-name="e"/>
   <glyph unicode="f" glyph-name="f"/>
   <glyph unicode="i" glyph-name="i"/>
   <glyph unicode="l" glyph-name="l"/>
   <glyph unicode="m" glyph-name="m"/>
   <glyph unicode="n" glyph-name="n"/>
   <glyph unicode="o" glyph-name="o"/>
   <glyph unicode="r" glyph-name="r"/>
   <glyph unicode="s" glyph-name="s"/>
   <glyph unicode="t" glyph-name="t"/>
   <glyph unicode="u" glyph-name="u"/>
   <glyph unicode="w" glyph-name="w"/>
My icon font represented in an SVG document.

Yeah, I know. That’s a lot to take in one go. That’s okay. I’ll explain. Assuming basic knowledge of SVG file structure the first thing that should be unfamiliar is the font element.

<font id="dWIcons" horiz-adv-x="0">
 <font-face font-family="dW Icons" font-weight="normal" font-style="normal" units-per-em="1000" ascent="500" descent="-500">
   <font-face-name name="dW Icons Regular"/>

In the excerpt above the font element has an id attribute and a horiz-adv-x attribute. The id attribute is the same as it is in HTML. The camel case structure of the id isn’t what is found in XML or HTML because it is used by software (to be detailed later) to generate name tables for the resultant TrueType font. The horiz-adv-x attribute is the default width in UPMs of a glyph; in this case it’s zero.

The font-face element defines the names and metrics for the fonts. The font-family, font-weight, and font-style attributes should all be self-explanatory. The units-per-em attribute shows exactly what was mentioned earlier. Lastly there’s the ascent and descent attributes. In a normal font they’re used to define the top of the ascenders and the bottom of the descenders; in this icon font they’re used to force the baseline in the center of the icon and to help with some residual math necessary when exporting SVG later.

There is one more name to use in a font for our purposes and that’s the font-face-name which goes inside a font-face-src element. It specifies the full name for this font which includes both the family name and its style name. Now, onto the glyphs that are within the font element.

<glyph unicode="ArtStation" glyph-name="artstation" horiz-adv-x="1000" d="M0,12,84-135c18-32,50-55,91-55H735L619,12Zm1000-2c0,21-7,39-16,55L655,636c-17,32-50,54-89,54H392L900-189,980-51c16,27,20,39,20,61ZM536,155,309,548,83,155Z"/>

As can be inferred by its name, the glyph element defines an individual glyph of the font. The particular example above is a ligature because its unicode attribute contains multiple characters. The glyph-name attribute is important for our uses because it is used to name individual glyphs which are used in the TrueType font’s ligature feature table. The horiz-adv-x attribute is the same as the one mentioned before, but here it is defined just for this glyph. Lastly, the d attribute contains path information in SVG’s esoteric way.

<glyph unicode="." glyph-name="period"/>

At the end of the font element there’s a series of glyphs which look like this. They are glyphs for the individual characters in each of the ligatures’ unicode attributes. There must be characters to replace present in the font, after all. The glyph contains no geometry and is intentionally empty.

Creating the Icons

While it definitely is possible to write SVG path data by hand I’m not going to do it, especially when it can be exported from a vector application such as Adobe Illustrator or Inkscape. In my explanations below I am going to be using Illustrator, but everything here is possible — and perhaps even easier — with Inkscape. Illustrator is what I personally am more familiar with, so I will be using that.

This is where the math, stuff about UPM units, and the ascent and descent attributes come together. Using this approach each icon must be a compound path and in one color. It would be helpful to have all points be integers, but it’s not absolutely necessary as the converter software will fix that; it’s still a good idea to change them before to eliminate potential problems.

Cropping of a screenshot of the icons and their artboards in Adobe Illustrator

Is this a mistake? Unfortunately, no. In the example above I’ve created 1000×1000 point artboards for each of the icons because I use 1000 UPM in the font, flipped them vertically, and then adjusted their positions within the artboard upward by 250 points (1/4 of UPM). In Adobe Illustrator CS5, Adobe flipped the vertical ruler direction to where down was positive and up was negative. In fonts it’s the opposite, so that’s why the icons are flipped. I’m not awfully sure about the other. I did a lot of testing, and this is what ended up working. Adobe Illustrator copies objects to the clipboard in SVG. You would think that would be the obvious way to get SVG path data from these, but you’d be wrong. Objects copied to the clipboard do not seem to care about ruler direction, ruler origin, or even its position within 2-D space; typical Adobe. Instead, export the artboards to SVG files and then copy the icon path’s d attribute to the corresponding glyph in the icon SVG font.

And, Finally…

If the SVG specification authors and browser vendors weren’t shortsighted this would be the end of the font generation process. We would have a font format as editable as a text document — perfect for icons. Sadly, we must instead carry on. The last step of the font creation process involves converting our icon SVG font into a TrueType font. The requirements are as follows:

  • Node.js, a requirement for svg2ttf as it is written in Node
  • svg2ttf, the CLI used to convert the SVG icon font to a TrueType font
  • ttfautohint, a CLI used to perform voodoo on the font add hinting to make it display a bit more nicely in Windows
brew install node ttfautohint
npm install -g svg2ttf

Node.js and ttfautohint are easily available in your system’s package manager. I use macOS, so above I use Homebrew. You might be using a different operating system, so look up if you even have a package manager or if they are available. If not, they can always be downloaded manually. Svg2ttf is available via npm which is installed with Node.js. With all the requirements met converting the font is simple:

svg2ttf --copyright "© 2018 Dustin Wilson" icons.svg icons.ttf
ttfautohint --symbol --hinting-limit=0 --windows-compatibility icons.ttf icons-hinted.ttf
mv icons-hinted.ttf icons.ttf

This above is using a bash shell, so if using Windows or a different shell the syntax might be a bit different. It would be convenient if the output of svg2ttf could be piped to ttfautohint, but the author of svg2ttf didn’t provide standard output capabilities. Svg2ttf also injects its own copyright information in the font but thankfully has an option to supply your own. Considering these tools are available with command line interfaces it should be trivial to automate the process further. Generating WOFF files is outside the scope of this tutorial, but the output TrueType file should work in any tools you use to convert other fonts.

Markup & Styling

Using the font is pretty much the same as you would any other font on the Web:

<nav aria-label="social">
  <li><a href="https://artstation.com/dustinwilson/" class="artstation">ArtStation</a></li>
  <li><a href="https://dribbble.com/dustinwilson/" class="dribbble">Dribbble</a></li>
  <li><a href="mailto:dustin@dustinwilson.com" class="email">Email</a></li>
  <li><a href="/feeds/" class="feeds">Feeds</a></li>
  <li><a href="https://github.com/dustinwilson/" class="github">GitHub</a></li>
  <li><a href="https://www.last.fm/user/DustinWilson/" class="lastfm">Last.fm</a></li>
  <li><a href="https://mastodon.dustinwilson.com" class="mastodon">Mastodon</a></li>
  <li><a href="https://twitter.com/dustinwilson/" class="twitter">Twitter</a></li>
An excerpt from this website’s HTML concerning the footer icons
@font-face {
    font-family: "dW Icons";
    src: url("../fonts/icons.woff2") format("woff2"),
         url("../fonts/icons.woff") format("woff"),
         url("../fonts/icons.ttf") format("truetype");
html {
    font-variant-ligatures: common-ligatures;
@supports (font-feature-settings: "liga") {
    body > footer a {
        font-family: "dW Icons", Ook, Verdana, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
        font-size: 1.5rem;
        line-height: 1;
        text-decoration: none;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        color: #9ea9b2;
        text-transform: none;
@supports not (font-variant-ligatures: common-ligatures) {
    body > footer a {
        font-feature-settings: 'liga', 'c2sc';
An excerpt from this website’s CSS concerning the footer icons (sans prefixed properties when superfluous)

There are a few notable differences, though. Ligatures need to be enabled for the font to do its thing, so I have set font-variant-ligatures on the entire page. Normally if ligatures aren’t supported and @font-face is then users would be presented with a blank footer; that’s not good. There are two @supports for this. One detects if ligatures are supported via font-feature-settings and applies the necessary styling to the footer, increasing the font size and changing the font rendering to a simple grayscale where it can. Normally I’m vehemently opposed to using those properties because on regular text it actually reduces legibility by decreasing the resolution of the type. It’s not necessary on icons, though. The other @supports enables ligatures via font-feature-settings if font-variant-ligatures isn’t supported. In my example the property enables both ligatures and capitals to small caps because elsewhere in the document c2sc is enabled, and font-feature-settings doesn’t stack.

Whew. That was a lot to write. The greatest thing about this approach is that the icons in the footer are just text. They’re selectable and readable by screen readers, and if ligatures aren’t supported it’s still just text. My hope is that this will be useful to designers and developers and give them one extra thing in their Web repertoire because that’s all it is — just one more way of doing something. It won’t work for all cases, but it’s good to know it’s there as an option.