DEV Community

Cover image for Why Custom Icon Fonts are the Ultimate Lightweight Icon Strategy
Supreet Pradhan
Supreet Pradhan

Posted on

Why Custom Icon Fonts are the Ultimate Lightweight Icon Strategy

Modern best practice leans heavily toward SVG. They’re flexible, accessible, and powerful — and in many cases, they’re absolutely the right choice.

But here’s the thing.

Sometimes you don’t need power.

You need simplicity.

You don’t need:

  • A full icon library
  • A component abstraction
  • A build-time SVG loader
  • Dozens (or hundreds) of unused icons in your bundle

You just need:

<span class="icon-search"></span>
Enter fullscreen mode Exit fullscreen mode

When Icon Fonts Are Actually the Simpler Choice

For small UI icon sets — especially in server-rendered or static projects, a single .woff2 file + one CSS file can be simpler and smaller than pulling in an SVG library. Fonts are aggressively cached, require no JavaScript, and behave like text by default.

Fonts are:

  • Aggressively cached by browsers

  • Pure CSS (no JavaScript required)

  • Naturally scalable

  • Text-like by default (inherit size and color automatically)

Instead of importing a large icon package, a minimal SVG → font pipeline can be set up using Node.js.

No frameworks.
No bundlers.
Just a small build script and a folder of SVGs.

Let’s break it down.


The Stack

  • Node.js (ESM, "type": "module")
  • svgtofont — the library that does the heavy lifting
npm install svgtofont --save-dev
Enter fullscreen mode Exit fullscreen mode

Project Structure

icons/             ← drop your SVGs here
dist/              ← generated font + CSS lands here
scripts/
  build-icons.mjs  ← the build script
package.json
Enter fullscreen mode Exit fullscreen mode

The Build Script

This is the entirety of scripts/build-icons.mjs:

import svgtofont from 'svgtofont';
import path from 'path';
import fs from 'fs';

const rootDir = process.cwd();

const ICONS_DIR = path.resolve(rootDir, 'icons');
const DIST_DIR = path.resolve(rootDir, 'dist');
const FONT_NAME = 'icons';

async function build() {
  await svgtofont({
    src: ICONS_DIR,
    dist: DIST_DIR,
    fontName: FONT_NAME,
    classNamePrefix: 'icon',   // ← generates .icon-search, .icon-cart, etc.
    css: true,                  // ← auto-generate the CSS file
    outSVGPath: false,
    generateInfoData: false,
    website: null,
    emptyDist: true,            // ← clear dist before each build
    startUnicode: 0xe001,       // ← start in the Private Use Area (PUA)
  });

  // svgtofont outputs some extra files we don't need — clean them up
  const allowed = ['.ttf', '.woff', '.woff2', '.css'];
  fs.readdirSync(DIST_DIR).forEach((file) => {
    if (!allowed.some((ext) => file.endsWith(ext))) {
      fs.unlinkSync(path.join(DIST_DIR, file));
    }
  });

  console.log('✅ Icons built successfully.');
}

build().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Wire it up in package.json:

{
  "type": "module",
  "scripts": {
    "build:icons": "node scripts/build-icons.mjs"
  }
}
Enter fullscreen mode Exit fullscreen mode

Run it:

npm run build:icons
Enter fullscreen mode Exit fullscreen mode

Output in dist/:

dist/
  icons.css
  icons.ttf
  icons.woff
  icons.woff2
Enter fullscreen mode Exit fullscreen mode

What Gets Generated

The icons.css looks like this:

@font-face {
  font-family: "icons";
  src: url('icons.woff2') format('woff2'),
       url('icons.woff')  format('woff'),
       url('icons.ttf')   format('truetype');
}

[class^="icon-"], [class*=" icon-"] {
  font-family: 'icons' !important;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-cart-shopping::before { content: "\e001"; }
.icon-eye::before           { content: "\e002"; }
.icon-facebook::before      { content: "\e003"; }
.icon-house::before         { content: "\e004"; }
.icon-plus::before          { content: "\e005"; }
.icon-volume::before        { content: "\e006"; }
Enter fullscreen mode Exit fullscreen mode

Each icon name maps directly to the SVG filename (without extension). The ::before pseudo-element injects the glyph via a Unicode codepoint in the Private Use Area — same technique Font Awesome uses.


Using It in HTML

<link rel="stylesheet" href="./dist/icons.css" />

<span class="icon-search"></span>
<span class="icon-cart-shopping"></span>
Enter fullscreen mode Exit fullscreen mode

Sizing and coloring works exactly like text:

.icon-search {
  font-size: 24px;
  color: #6152e8;
}
Enter fullscreen mode Exit fullscreen mode

Used correctly, an SVG icon and a generated font icon can look visually identical in the UI. The difference is not in appearance, but in delivery — a single cached font file can sometimes be simpler and lighter than managing multiple inline SVGs.


Wrapping Up

The whole thing is about 35 lines of JavaScript.

It’s also important to use clean SVG files with a proper viewBox, simple path elements, and no embedded images. If you’re working with outline-style icons, convert strokes into filled outlines before generating the font.

Custom icon fonts are not a replacement for SVG in every situation. But for small, controlled icon sets in static or server-rendered projects, they can be a very lightweight, cache-friendly, and dependency-free solution.

The full source code GitHub


References -
Icons sourced from Font Awesome
icon-font vs svg Blog
The Developer's Guide to Icons: Icon Fonts vs. SVGs Blog

Top comments (0)