Date: 2024-04-01

Status: accepted

Context

Deciding how to implement and use icons in your project might not sound like a big deal, but it can be a big pain point if done wrong (and there are many ways to do it wrong).

Implementing icons (technology-wise)

Bad (but common) approaches

  • Image + SVG - using img tag with an external .svg file as source:

    <img src="icon.svg" />

    Major tradeoffs:

    1. bad initial loading experience - the first time the page is downloaded there is a flicker before the icons render
    2. limited design options - the svg cannot be styled using CSS if its in an img tag
  • Inline SVG - the SVG is explicitly part of the bundled code:

    const icon = (
      <svg viewBox="0 0 24 24" width={16} height={16}>
        <path d="..." />
      </svg>
    );

    Major tradeoffs:

    1. HTML document is bloated, which slows down memory performance
    2. JS Bundle is bloated, hurting the application’s performance

    Same thing goes for some external icon libraries like Material UI - its just a nicer abstraction but the resulting HTML (and performance consequences) are the same.

    import DeleteIcon from '@mui/icons-material/Delete';
    
    <DeleteIcon />;
  • Icon font - icons are rendered as part of a font (either a public icon font like font-awesome, or a custom-built icon font):

    <html>
      <head>
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
      </head>
      <body>
        <i class="fa fa-camera-retro"></i>
      </body>
    </html>

    Major tradeoffs:

    1. bad initial loading experience - on first load, there can be a flash of unstyled text before the icon font loads properly
    2. glyph collision - since icon fonts use character codes, conflicts can occur with actual text content, leading to errors like wrong icon / no icon displayed.
    3. limited design options - icon fonts are monochrome, in contrast to SVGs which support multi-color and complex designs.
    4. maintainability/scalability - adding or customizing icons requires generating a new icon font file, which is a cumbersome process (compared to editing/adding an SVG file), and can lead to conflicts in code.

Better approaches

  • Tree-shakable icon (SVG) libraries - the result is the same as the “inline SVG” approach, but with better abstractions and with only the icons that are imported into the project being included in the final bundle (via the process of tree-shaking).

    import { Trash } from 'lucide-react';
    
    <Trash color="red" size={12} />;
  • Inline SVGs using SVG Sprites - all SVGs are part of a single big SVG file and then individually referenced. since its only one file, we can even preload it for better performance.

    <html>
      <head>
        <link rel="preload" as="image/svg+xml" href="sprite.svg">
      </head>
      <body>
        <svg><use href="/sprite.svg#circle"></svg>
      </body>
    </html>

Choosing the actual icons

Icons should be easly recognizable, consistent in their styling, readable/accessible customizable and scalable.

There are many open-source resources that provide icons that follow the same design rules. Some are easier to use and implement, some provide a richer set of icons, and some provide good customization abilities, but its hard to find one match that answers all of these needs.

IMO, we should focus on choosing a library that has a vast selection of icons (so we don’t find ourselves having to use multiple sources of icons very fast, which would hurt the consistency aspect), and that the actual icons are both recognizable and look good and “edgy” (unlike material UI).

Here are some of the examples I found and their advantages/tradeoffs:

  • Geist Design System Icons

    Advantages:

    Tradeoffs:

    • Leans very heavily towards being used as an npm library, confining our implementation options
    • Latest version was from 2 years ago πŸ˜…

    Sadly, its unclear whether the icon of Vercel’s “Geist” design system are free to use commercially, as they don’t have a dedicated Github repository with license like their “Geist” font (seems like they have a private npm library @geist/new-icons). Also, Vercel is a full-fledged company therefore their design system is not community driven, meaning we can’t rely on which icons they will add/not in the future.

  • Fontawesome Icons

    Advantages:

    • An incredibly diverse library of icons - there’s a very slim chance you fon’t find the icon you want there
    • Icons have many different styles and can be highly customized

    Tradeoffs:

    • Only part of the icons in the library are free to use
    • Customization abilities also require “pro” account
    • Fontawesome lean very heavily to their icons being used as an icon font - essentially confining us to choose this implementation (although its possible to also download the free icons as SVGs)
  • RadixUI Icons

    Advantages:

    • Fully open-source and free to use.
    • Available both as a React library and as separate SVG files
    • Great DX, easy to add new (download with sly, like epic stack’s implementation)
    • It makes sense to use them as we’re using RadixUI for our headless UI.

    Tradeoffs:

    • Relatively small set of icons, most of them are irrelevant to our kind of product
    • Latest version was from 2 years ago πŸ˜… so we can’t count on them to add new ones
  • Lucide Icons

    Advantages:

    • Fully open-source and free to use.
    • Available both as a React library (with treeshaking!) and as separate SVG files
    • Great DX, easy to add new (download with sly)
    • Very customizable - you can even choose different stroke widths for the icons
    • Community-driven library, meaning new icons will continue to be added per community requests.
    • A relatively vast selection of icons

    Tradeoffs:

    • DX of adding new icons is not as strong as Radix UI’s icons

Decision

  • Use the Lucide as our icon library
  • Bundle the icons in a SVG sprite implementation format
  • Use sly CLI tool to download the icons and manipulate them for us
  • Base our implementation on epic stack’s implementation (but use lucide icon library instead of radix)

See more info about using and adding to our icons library in services/remix/other/svg-icons/README.md

Consequences