Icon

Renders an SVG icon with optimal performance.

Usage

IconExample.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
<P>
This icon has the same size as this font: <Icon name="avatar" size="font" label="Account" className="text-emerald-500" />
</P>
<Icon name="avatar" size="sm" label="Account" className="text-emerald-500" />
<Icon name="avatar" size="md" label="Account" className="text-emerald-500" />
<Icon name="avatar" size="lg" label="Account" className="text-emerald-500" />

Here's the <Icon /> component in action.

This icon has the same size as this font: Account

AccountAccountAccount

Features

  • Uses SVG sprites for optimal icon performance.
  • Comes with a script to automatically generate the SVG sprite.
  • The icon defaults to the size of the font.
  • Accepts a size prop to set the icon size.
  • Accepts a label prop to add an accessible label.

Usage notes

SVG sprites are useful if you want to style them via CSS, like changing the foreground color with text-emerald-500 for example. If you want to load complex SVGs, use standard images instead with a simple <img src>.

Workflow

Here's the workflow for adding new SVG sprites to the <Icon> component:

  1. Add a new SVG icon to the ./icon/svgs folder that comes with this component
  2. Run npm run build:icons to update the SVG sprite
  3. Use the new icon

Installation

  1. Install the icon component

    Download and unzip the file into the /app/components/media folder

  2. Install the <VisuallyHidden> component
  3. Install the tailwind-merge.ts file

    tailwind-merge.ts
    1. 1
    2. 2
    3. 3
    4. 4
    5. 5
    6. 6
    7. 7
    8. 8
    9. 9
    10. 10
    11. 11
    12. 12
    13. 13
    14. 14
    import { type ClassValue, clsx } from 'clsx'
    import { extendTailwindMerge } from 'tailwind-merge'
    const twMerge = extendTailwindMerge<string, string>({
    extend: {
    classGroups: {
    colors: [{ 'text-muted': ['50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950'] }, 'text-ring'],
    spacing: [{ 'text-fluid': ['6xl', '5xl', '4xl', '3xl', '2xl', 'xl', 'lg', 'md', 'sm', 'xs', 'tooltip'] }],
    },
    },
    })
    export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs))
  4. Install Prettier (if you haven't).

    Run npm i -D prettier

    built-icons.js uses it to format the generated files.

  5. Add this to your package.json scripts

    "build:icons": "node app/components/media/icon/build-icons.js"

  6. Check if everything is working

    Run npm run build:icons
  7. Finally make sure to preload your sprite.svg file:

    root.tsx
    1. 1
    2. 2
    3. 3
    <head>
    <link rel="preload" as="image/svg+xml" href="path/to/sprite.svg">
    </head>

Depending on your server’s configuration, you might need to make sure the proper cache-headers are set on your sprite.svg so the browser can cache it appropriately.

Source

icon.tsx
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
import { type SVGProps } from 'react'
import { cn } from '#app/utils/tailwind-merge.ts'
import { type IconName } from './icon/name.js'
import href from './icon/sprite.svg'
export type IconProps = SVGProps<SVGSVGElement> & { name: IconName; label?: string; size?: 'font' | 'sm' | 'md' | 'lg'; children?: never }
/**
* Renders an SVG icon with optimal performance.
*/
const Icon = ({ name, label, size = 'font', className, ...props }: IconProps) => {
return (
<>
<svg
aria-hidden="true"
focusable="false" // See: https://allyjs.io/tutorials/focusing-in-svg.html#making-svg-elements-focusable
{...props}
className={cn('icon', size, className)}
>
<use href={`${href}#${name}`} />
</svg>
{label ?
<span className="sr-only">{label}</span>
: null}
</>
)
}
export { Icon, IconName, href }

Styling

tailwind.css
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
.icon {
@apply inline;
/* Sizes */
&.font {
@apply h-[1em] w-[1em];
}
/* Sizes */
&.sm {
@apply h-fluid-12 w-fluid-12;
}
&.md {
@apply h-fluid-14 w-fluid-14;
}
&.lg {
@apply h-fluid-15 w-fluid-15;
}
}