How to add icons to SolidJS apps

The easiest way would be to use the Icon component provided by your UI library of choice. In my case, that would have been to use the Icon component of Hope-UI. Given that Hope-UI doesn’t include any icons by default, they give you a basic Icon wrapper to either import one from a library or create your own.

Being the newbie I am, I thought it would be easier to go with the latter initially: just for the couple of icons I wanted to display the effort to look for an icon library and install it seemed overwhelming.

// IconBell.tsx (HiOutlineBell from HeroIcons library)
import { Icon } from "@hope-ui/solid";

export function IconBell(props) {
  return (
    <Icon viewBox="0 0 200 200" {...props}>
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path>
    </Icon>
  );
}

// main.ts
import { IconBell } from './IconBell';
//...
<IconBell />

So you basically have to manually transform each icon before using it. But I’m so lazy when it comes to repetitive tasks that I thought to myself:

Copy-pasting SVG inner elements from some icon file to your own icon component seems such a waste of energy. Why don’t I automate it? I only need to (1.1) pass the icon name to my icon component, then (1.2) make it load the icon file, then (1.3) make it display the icon markup, et voilà. Tidy!
How hard can it be?

Quite hard indeed. In fact, so hard to make #1.3 work that after many hours of tinkering I concluded it wasn’t possible in SolidJS. I even explored some esoteric concepts like Tagged Template Literals to no avail.

Back to square one, I looked for icons libraries for SolidJS and discovered that someone already did the SVG to SolidJS component conversion for thousands of Open Source icons from many different libraries, and collected them into the solid-icons library. With that installed, things get much easier.

// main.tsx
import { HiOutlineBell as IconBell } from 'solid-icons/hi';
//...
<IconBell />

And that would be it, but you still have to deal with the facts that (2.1) each icon is its own component, (2.2) not integrated with your UI library. For (2.1), that means to import icons one by one while sticking to a naming convention like Icon<name>, to help you at keeping track of them project-wide. For (2.2), that means to wrap each icon into some basic component of your UI library of choice, to be able to format the icon to your needs using the same design system as any other component.

Hope-UI’s Icon component solves #2.2.

// main.tsx
import { Icon } from "@hope-ui/solid";
import { HiOutlineBell } from 'solid-icons/hi';
// ...
<Icon as={HiOutlineBell} />

My Icon component solves #2.1 too.

// Icon.tsx
import { splitProps } from "solid-js";
import { Dynamic } from "solid-js/web";
import { JSX } from "solid-js/jsx-runtime";
import { Box } from "@hope-ui/solid";
import * as hi from "solid-icons/hi";

export function Icon(props) {
  const [local, rest] = splitProps(props, ["name"]);
  return (
    <Box style={{ display: "inline-block" }} {...rest}>
      <Dynamic component={hi[local.name as string] as JSX.Element} />
    </Box>
  );
}

// main.tsx
import { Icon } from "./Icon";
//...
<Icon name="HiOutlineBell" />

Tree shaking shouldn’t be a concern according to this thread. However, you could just register all needed icons in a single import/export IconsRegistry file and later import * from "./IconsRegistry".


But then I remembered, out of the blue, a piece of SolidJS internal code I had seen in some issue days ago.

export namespace JSX {
  type Element =
    | Node
    | ArrayElement
    | FunctionElement
    | (string & {})
    | number
    | boolean
    | null
    | undefined;

where the JSX.Element type is just a union of many different types, including a real Node element type, and that is exactly what I needed for #1.3 !!

Here is my Icon component to load the SVG from a file and display it. Hurrah!

// BareIcon.tsx
import { Box } from "@hope-ui/solid";
import { splitProps, createResource, createSignal } from "solid-js";

function fragment(html: string) {
  const tpl = document.createElement("template");
  tpl.innerHTML = html;
  return tpl.content;
}

async function importIcon(filename) {
  const module = (await import(`../icons/${filename as string}.svg`)) as {
    default: string;
  };
  return module.default;
}

export function BareIcon(props) {
  const [local, rest] = splitProps(props, ["filename"]);
  const [filename, setFilename] = createSignal("");
  const [svg] = createResource(filename, importIcon);
  // eslint-disable-next-line solid/reactivity
  setFilename(local.filename);
  return (
    <Box style={{ display: "inline-block" }} {...rest}>
      {svg.loading ? "" : fragment(svg())}
    </Box>
  );
}

// main.tsx
import { BareIcon } from "./BareIcon";
//...
<BareIcon filename="bell" color="red" />

To make it work, you’ll need to install the Vite SVG loader plugin, and configure it to always load SVG files in RAW mode.

// vite.config.js
import { defineConfig } from 'vite';
import solidPlugin from 'vite-plugin-solid';
import eslintPlugin from 'vite-plugin-eslint';
import svgLoader from 'vite-svg-loader';

export default defineConfig({
  plugins: [
    solidPlugin({
      dev: true
    }),
    eslintPlugin(),
    svgLoader({
      defaultImport: 'raw'
    })
  ],
  //...
});