# Low-tech icon setup in react

Super simple but manual icon system.

There are a few ways to do icons in a React project. What irks me in some of the
keys-in-hand icon systems is the package install size, several hundreds of
megabytes. You of course don't load all of that in the frontend. For a more
complete look at the tradeoffs of React SVG icons, read
[the guide to handling icons as React components](/how-to-do-icons-in-react).

I've tried few things and somehow settled into a very low tech and manual system
which has served me well.

## Icon sprite

```xml title="sprite.svg" caption="Place the icon in yout public dir"
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0">
  <symbol id="arrow-left-bar">
    <path d="M21 12h-18" />
    <path d="M6 9l-3 3l3 3" />
    <path d="M21 9v6" />
  </symbol>
  <symbol id="arrow-left-to-line">
    <path d="M3 19V5" />
    <path d="m13 6-6 6 6 6" />
    <path d="M7 12h14" />
  </symbol>
  <symbol id="arrow-right">
    <path d="M5 12l14 0" />
    <path d="M13 18l6 -6" />
    <path d="M13 6l6 6" />
  </symbol>
  <!-- more icons... -->
</svg>
```

## Icon component

```tsx title="Icon.tsx"
import type { ComponentProps } from 'react'

// Update this when adding new icons
export type IconNames = 'arrow-left-bar' | 'arrow-left-to-line' | 'arrow-right'

// React doesn’t include types for data attributes
type DataAttributes = Record<
  `data-${string}`,
  string | boolean | number | undefined
>

export interface IconProps
  extends
    Partial<
      Omit<ComponentProps<'svg'>, 'width' | 'height' | 'name' | 'viewBox'>
    >,
    DataAttributes {
  isFilled?: boolean
  label?: string
  name: IconNames
  size?: number
}

const filledProps = { fill: 'currentColor', strokeWidth: 0 } as const
const outLineProps = {
  fill: 'none',
  stroke: 'currentColor',
  strokeLinecap: 'round',
  strokeLinejoin: 'round',
} as const

// The svg sprite is lives in public dir
export function Icon({
  name,
  label,
  size = 16,
  strokeWidth = 2,
  isFilled,
  ...props
}: IconProps) {
  return (
    <svg
      // You can choose to fill an icon, some icons work filled, other don't
      {...(isFilled ? filledProps : { ...outLineProps, strokeWidth })}
      {...props}
      aria-hidden={!label}
      height={size}
      viewBox='0 0 24 24'
      width={size}
    >
      {!!label && <title>{label}</title>}
      <use href={`/sprite.svg#${name}`} />
      {/* Animate the load spinner */}
      {name === 'loader' && (
        <animateTransform
          attributeName='transform'
          dur='1s'
          repeatCount='indefinite'
          type='rotate'
          values='0;360'
        />
      )}
    </svg>
  )
}
```

## Preload it

```html
<link as="image" href="/sprite-17.svg" rel="preload" type="image/svg+xml" />
```

In react-router you can use the `meta` utility:

```ts
export const meta = () => {
  return [
    {
      as: 'image',
      href: `/sprite.svg`,
      rel: 'preload',
      tagName: 'link',
      type: 'image/svg+xml',
    },
  ]
}
```

## Adding icons

1. Go to [lucide.dev](https://lucide.dev/icons/ellipsis)
2. Select an icon you want
3. Click "Copy SVG"
4. Paste it into your `sprite.svg`
5. Replace `<svg>` with a `<symbol id="zoom-out">`
6. Update `IconNames` type in `Icon.tsx`

The SVG:

```xml
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis-icon lucide-ellipsis">
  <circle cx="12" cy="12" r="1"/>
  <circle cx="19" cy="12" r="1"/>
  <circle cx="5" cy="12" r="1"/>
</svg>
```

Replaced with `<symbol>`:

```xml
<symbol id="ellipsis">
  <circle cx="12" cy="12" r="1"/>
  <circle cx="19" cy="12" r="1"/>
  <circle cx="5" cy="12" r="1"/>
</symbol>
```

## Busting the caches

You should have far expiring cache headers for the `sprite.svg`. The best and
the simplest way to bust the caches in a situation like this, is to rename the
sprite to, say, `sprite-2.svg`.

Then do a search and replace on your codebase for `sprite.svg` > `sprite-2.svg`.

## Who's it for?

Not for enterprise, more for solo devs or small teams. There's no hassle related
to icon build step or extra deps, you trade simplicity to manual setup.
