DEV Community

Cover image for How to Convert SVGs into Clean Web Icon Fonts Entirely in the Browser (No Node, No Canvas, No Server Uploads)

How to Convert SVGs into Clean Web Icon Fonts Entirely in the Browser (No Node, No Canvas, No Server Uploads)

A while back I needed a custom icon font for a side project. Nothing fancy — maybe 30 icons, all SVGs I had already exported from Figma. The "proper" way to do this is apparently to install some Node toolchain, run a CLI, and hope the output isn't garbage. I didn't want any of that. My whole app runs client-side, so why should the font pipeline need a server?

Turns out, you can build a real, valid TTF icon font in pure browser JavaScript. No canvas, no Node, no uploading your SVGs to someone's server. This post walks through exactly how I did it, with working code.

If you're in a hurry, I ended up polishing the whole thing into a tool you can use right now: devomnitools.com/en/tools/svg-to-font/. But stick around if you want to know how it actually works under the hood — it's less magic than you'd think.

Why bother with icon fonts at all in 2026?

Fair question. SVG sprites and inline SVGs are great, and for most cases I'd reach for those first. But icon fonts still win in a few specific situations:

  • You need legacy / standalone embedded icon support
  • You're theming via color and font-size — with icon fonts, currentColor just works, no CSS variables gymnastics
  • Huge icon sets where inline SVGs would bloat your HTML DOM for no reason

Also, honestly, it's just a fun engineering problem to solve.

The two libraries that make this possible

Everything hinges on two battle-tested libraries:

  1. svgpath — parses SVG path data (d attributes) and lets you transform it mathematically.
  2. opentype.js — a full font compiler that runs directly in JavaScript. It can build fonts from scratch, not just read them.

That's it. No canvas needed, because we never rasterize anything — a font glyph is vector data, so the conversion is mostly just moving numbers around.


Step 1: Get clean path data from your SVGs

The first gotcha: an icon font only understands path outlines. Your SVG might contain <circle>, <rect>, <line>, strokes with round caps — a font glyph has none of that vocabulary. Everything must be a single filled path (or a few paths combined).

If you're exporting from Figma/Illustrator/Inkscape, use "Outline Stroke" before export. In Figma it's Shift + Ctrl/Cmd + O. Do this before exporting, and 90% of your pain disappears.

Then, in the browser, read the file and extract all <path> elements:

async function svgToPathData(file) {
  const text = await file.text();
  const doc = new DOMParser().parseFromString(text, "image/svg+xml");
  const paths = [...doc.querySelectorAll("path")]
    .map(p => p.getAttribute("d"))
    .filter(Boolean);

  if (paths.length === 0) {
    throw new Error(`${file.name}: no <path> elements found. Outline your strokes first!`);
  }

  return paths.join(" "); // multiple subpaths are fine in one glyph
}
Enter fullscreen mode Exit fullscreen mode

Heads up: If your SVG has transforms on the paths (transform="rotate(...)" etc.), you need to apply them to the path data or your icons will come out rotated/skewed. The svgpath library can do this, but cleaning up in your vector editor beforehand is usually less debugging.


Step 2: Normalize everything to the em square

Fonts are drawn on an em square — typically 1000 units tall for TTFs. Your SVGs have their own random viewBox, maybe 24x24, maybe 48x48. If you skip this step, every glyph will be a different size and your icons will look misaligned.

So for each icon: read the viewBox, compute the scale factor, and resize with svgpath:

import svgpath from "https://esm.sh/svgpath";

const EM_SIZE = 1000;

function normalizePath(d, viewBox) {
  const [,, vbW, vbH] = viewBox;
  const scale = EM_SIZE / Math.max(vbW, vbH);

  return svgpath(d)
    .scale(scale)
    .translate(0, EM_SIZE)
    .scale(1, -1) // flip Y to font coordinate space
    .round(2)
    .toString();
}
Enter fullscreen mode Exit fullscreen mode

One thing that bit me: SVG's Y-axis points down, but font coordinates point up! For a 24-unit viewBox mapped to a 1000-unit em, you flip with .translate(0, EM_SIZE).scale(1, -1). If your icons render upside down (they will the first time), that's why!


Step 3: Build glyphs and compile the font

Now the fun part. opentype.js lets you define glyphs as path strings and compiles a valid TTF:

import opentype from "https://esm.sh/opentype.js";

function buildFont(icons) {
  // icons = [{ name: "home", pathData: "M0 0L..." }, ...]
  const notdef = new opentype.Glyph({
    name: ".notdef",
    unicode: 0,
    advanceWidth: EM_SIZE,
    path: new opentype.Path(),
  });

  const glyphs = icons.map((icon, i) => {
    const codepoint = 0xE000 + i; // Private Use Area — no emoji conflicts
    return new opentype.Glyph({
      name: icon.name,
      unicode: codepoint,
      advanceWidth: EM_SIZE,
      path: opentype.Path.fromSVG(icon.pathData),
    });
  });

  const font = new opentype.Font({
    familyName: "MyIcons",
    styleName: "Regular",
    unitsPerEm: EM_SIZE,
    ascender: EM_SIZE * 0.8,
    descender: -(EM_SIZE * 0.2),
    glyphs: [notdef, ...glyphs],
  });

  return font; // font.toArrayBuffer() gives you raw TTF bytes
}
Enter fullscreen mode Exit fullscreen mode

Download it directly in the browser as a Blob:

function downloadFont(font) {
  const buffer = font.toArrayBuffer();
  const blob = new Blob([buffer], { type: "font/ttf" });
  const url = URL.createObjectURL(blob);
  const a = Object.assign(document.createElement("a"), {
    href: url,
    download: "my-icons.ttf",
  });
  a.click();
  URL.revokeObjectURL(url);
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Actually use the font with generated CSS

Generate a tiny CSS file alongside the TTF, mapping each icon to its Unicode codepoint:

function generateCss(icons) {
  const rules = icons.map((icon, i) => {
    const code = (0xE000 + i).toString(16).toUpperCase();
    return `.icon-${icon.name}:before { content: "\\${code}"; }`;
  });

  return `
@font-face {
  font-family: "MyIcons";
  src: url("my-icons.ttf") format("truetype");
  font-weight: normal;
  font-style: normal;
}

[class^="icon-"]:before, [class*=" icon-"]:before {
  font-family: "MyIcons";
  display: inline-block;
  speak: never; /* don't let screen readers read out codepoints */
}

${rules.join("\n")}`;
}
Enter fullscreen mode Exit fullscreen mode

Then in HTML it's just:

<i class="icon-home"></i>
<i class="icon-settings" style="font-size: 24px; color: rebeccapurple;"></i>
Enter fullscreen mode Exit fullscreen mode

Where this approach falls short (being honest)

  1. No auto-hinting: Dedicated desktop font tools hint glyphs for tiny render sizes. For icons inside modern high-DPI displays this rarely matters, but text fonts would suffer.
  2. Compound paths: Overlapping paths with fill-rule="evenodd" can render unexpectedly in some legacy renderers. Always test your exported icons.
  3. WOFF2 output: Not built into opentype.js — you get TTF and WOFF. For most icon sets, TTF + WOFF is plenty for browser usage.
  4. No advanced font mastering: Kerning tables, ligatures, variable axes are out of scope. It's an icon font, not Helvetica!

TL;DR

  1. Export SVGs with strokes outlined.
  2. Parse with DOMParser, extract d attributes.
  3. Normalize to a 1000-unit em square with svgpath (and flip the Y axis!).
  4. Compile with opentype.js glyphs.
  5. Download as a blob, ship with a generated CSS file.

The whole pipeline is under 150 lines of vanilla JavaScript.

If you'd rather skip the plumbing and just get a font, my finished version handles multi-file upload, naming, live glyph preview, and CSS generation — it's free and everything stays in your browser with zero server data leakage:
👉 devomnitools.com/en/tools/svg-to-font/

Have you built anything with opentype.js? I keep finding new uses for it — font subsetting in the browser is probably my next rabbit hole. Let me know in the comments what you'd build!

Top comments (0)