Johan wrote this
@ 2021-11-23

Subsetting Material Icons in Rails

Unless you’re cool enough to commission your own icon set, you’re probably using a library like Font Awesome or Google’s Material Icons. We’re using Material Icons for the new thing we’re building. They’re good! I’ve always found fonts to be the easiest way to serve icons: you can just slap them in like all the other text on the page and they can be styled and animated with CSS. The classic way of going about it is to hide icons in the unicode Private Use Area and then extract them via a big ol’ stylesheet that extracts every code point in a class with a content rule. A bit hairy to set up but works well in practice. All you need to know is what the class is called. Easy peasy.

The folks at Google with their unlimited farting-around time and monopoly billions have gone even further and added ligatures for their icon font, which is above and beyond. No special stylesheet needed, bring in the font and a single class for styling and then all you gotta do is <span class="material-icons">download</span> and the ligature for download will translate to the code point you want. All modern browsers and operating systems support ligature fonts these days. It’s practically magic.

The woff2 version of their icon font comes in at 42KB which is pretty small, all things considered, but, ah. I have a perfectionist streak when it comes to stylesheets and fonts. I always want to self-host (a whole blog post in itself which boils down to don’t add another single point of failure to your app if you can help it) and I don’t want my users to pay the full download tax for EVERY icon in that pack if I’m only using like 15 of them. Let’s roll up our sleeves:

class Icon
  CODE_POINTS = {
    add_circle: "e148",
    chevron_left: "e5cb",
    chevron_right: "e5cc",
    close: "e5cd",
    download: "e2c4",
    expand_less: "e5ce",
    expand_more: "e5cf",
    info: "e88e",
    remove_circle: "e15d",
    share: "e80d",
  }.freeze
end

That right there is your brand-new Icon class! Those are the only icons we want in the font we send to users. “But Johan” I hear you say, “how did you name them and what are those hex numbers, I’m feeling very lost and alone” and the easy answer is that you can find names and code points on the Material Icons homepage here. Click on the icon you want and its name and hex code will pop up in the sidebar. Copy any icons you want to your Icon class. You can technically name them whatever you want but I’d strongly recommend going with Google’s default names so you can use their reference site to look them up. Next step is a rake task for subsetting the font with only the icons we want:

namespace :fonts do
  desc "Generate a subset WOFF variant of the material icons we need"
  task subset: :environment do
    file = Rails.root.join("font-source/MaterialIconsOutlined-Regular.otf")
    code_points = Icon::CODE_POINTS.values.join(",")
    cmd = "fonttools subset #{file} --unicodes=#{code_points} --no-layout-closure --output-file=o.woff2 --flavor=woff2"

    system(cmd) && FileUtils.mv("o.woff2", "app/assets/fonts/icons.woff2")
  end
end

Note that you’ll need fonttools installed for this to work, and a font-source directory in your app containing the original Google font file. Whenever you add or remove an icon, run this rake task and it’ll create the subset font for you. You can find it in assets/fonts/icons.woff2 and it will be INCREDIBLY tiny – I’m up to 25 icons in my project now, and the font is 1.8KB. Small enough to inline as Base64 in your main CSS if you want, saving a roundtrip and FOUT. Just saying.

The final piece of the puzzle is the view helper:

module IconsHelper
  def icon(name, **args)
    arguments = { class: "icon" }.merge(args) { |_k, o, n| [o, n].compact.join(" ") }
    content_tag(:span, arguments) { "&#x#{Icon::CODE_POINTS.fetch(name.to_sym)};".html_safe }
  end
end

Wow, that’s an ugly one! Almost looks like perl. But those two lines will enable you to write <%= icon "download" %> in your views and get a sweet download icon. And it’ll work like you would expect a rails tag helper to work, so <%= icon "download", class: "green", style: "background:#f00" %> is valid too.

Now go forth and self-host your icon fonts! I believe in you.