SVG Icons with TailwindCSS and React

SVG Icons with TailwindCSS and React

Why I want to manage Icons the way I do ?

While ago I loved using library like ChakraUI, because it was all CSS in JS and I could manage colors, shapes and other things with only javascript. It was good but I wanted to go back to CSS because if I wanted to work with someone, the person needed to learn the ChakraUI syntax which is very specific and different from css.

<Box bg="tomato" w="100%" p={4} color="white">
  This is the Box
</Box>

It's very easy to make someone use tailwindcss because it's just css instructions as classname. for the same box here is the TailwindCSS code.

<div className="w-full bg-[tomato] p-4 text-white">This is a Box</div>

It's almost the same things to write but as string in a className prop. But it's not the same at all to use. Even today I prefer the flexibility of CSS in JS than CSS Files. However for my projects in group tailwindcss is a lot better. I continue to use CSS in JS for ReactNative apps with NativeBase

And all that to say that I love how ChakraUI manage SVG Icons. It's easy, super convenient and compatible with all SVG you have.

Recreate the ChakraUI Icon component.

It's pretty easy, it's just a <svg> wrapper tag that reset size to be the current text size (1em). Reset the line height the same way and default set display to inline-block. So I decided to copy/paste their code and adapt it to use tailwindcss with RadixUI.

The first step is the Icon component should be a <svg/> tag with the className to reset the svg.

import React, { Children } from "react";
import { cn } from "~/utils/tailwind";
 
export type IconProps = React.ComponentPropsWithoutRef<"svg">;
 
export const Icon = React.forwardRef<React.ElementRef<"svg">, IconProps>(
  ({ className, ...props }, ref) => {
    return (
      <svg
        ref={ref}
        className={cn(
          "inline-block h-[1em] w-[1em] align-middle leading-[1em]",
          className,
        )}
        {...props}
      />
    );
  },
);
Icon.displayName = "Icon";

cn is a function that combine tailwind-merge (Utility function to efficiently merge Tailwind CSS classes in JS without style conflicts) and clsx (A tiny utility for constructing className strings conditionally). You can see how its made here

Now we have a first version that can be use like below, you copy your SVG component and just change svg tag with Icon.

Default Example

Same size as text.

It's already pretty cool but, as you can see in the code, we always need to copy the SVG and content several times or need to make another component. And most of the time, we use a library or an import from an svg asset. So it would be cool if the icon component can take a children and send all is props to it. That's the goal of Radix Slot primitive.

Here is how to modify our current component to add @radix/react-slot:

import { Slot } from "@radix-ui/react-slot";
import React, { Children } from "react";
import { cn } from "~/utils/tailwind";
 
export type IconProps = React.ComponentPropsWithoutRef<"svg"> & {
  asChild?: boolean;
};
 
export const Icon = React.forwardRef<React.ElementRef<"svg">, IconProps>(
  ({ asChild, className, ...props }, ref) => {
    const Comp = (asChild ? Slot : "svg") as "svg";
 
    return (
      <Comp
        ref={ref}
        className={cn(
          "inline-block h-[1em] w-[1em] align-middle leading-[1em]",
          className,
        )}
        {...props}
      />
    );
  },
);
Icon.displayName = "Icon";

Here is an example that use our <Icon /> component with a lucide-react icon.

As a child Example

Same size as text.

Don't forget asChild.

If you forget the asChild props you can see that the <SmileIcon /> is render in another top-level <svg /> component so it can't render well.

Now that we can do both of them, what I loved about ChakraUI is that I could create a file named icons and put all my icons in the same file to use them whenever and wherever I wanted.

For this ChakraUI had a utility function createIcon with a lot of options. Here they are:

interface CreateIconOptions {
  /**
   * The icon `svg` viewBox
   * @default "0 0 24 24"
   */
  viewBox?: string;
  /**
   * The `svg` path or group element
   * @type React.ReactElement | React.ReactElement[]
   */
  path?: React.ReactElement | React.ReactElement[];
  /**
   * If the `svg` has a single path, simply copy the path's `d` attribute
   */
  d?: string;
  /**
   * The display name useful in the dev tools
   */
  displayName?: string;
  /**
   * Default props automatically passed to the component; overwritable
   */
  defaultProps?: IconProps;
}

The goal of this function is only to quickly create components that use our already made <Icon/> component and return a simple component for our icon. It's cool to use when working with a design team that work on figma for example, you just need copy their icon as SVG and paste it in the createIcon with the name and the size.

Here is the entire code for the createIcon utility function.

interface CreateIconOptions {
  /**
   * The icon `svg` viewBox
   * @default "0 0 24 24"
   */
  viewBox?: string;
  /**
   * The `svg` path or group element
   * @type React.ReactElement | React.ReactElement[]
   */
  path?: React.ReactElement | React.ReactElement[];
  /**
   * If the `svg` has a single path, simply copy the path's `d` attribute
   */
  d?: string;
  /**
   * The display name useful in the dev tools
   */
  displayName?: string;
  /**
   * Default props automatically passed to the component; overwritable
   */
  defaultProps?: IconProps;
}
 
export function createIcon(options: CreateIconOptions) {
  const {
    viewBox = "0 0 24 24",
    d: pathDefinition,
    displayName,
    defaultProps = {},
  } = options;
  const path = Children.toArray(options.path);
 
  const Comp = React.forwardRef<React.ElementRef<typeof Icon>, IconProps>(
    (props, ref) => (
      <Icon ref={ref} viewBox={viewBox} {...defaultProps} {...props}>
        {path.length ? path : <path fill="currentColor" d={pathDefinition} />}
      </Icon>
    ),
  );
 
  Comp.displayName = displayName;
 
  return Comp;
}

Now let's create the same code as previous examples but with a an icon created with this function.

createIcon Example

Same size as text.

Now you know how I manage svg icons in my react applications with tailwind. I hope this example helped you and do not hesitate to let me know if you use the same <Icon /> component in your project.

Any feedback ?

If there is any problem of comprehension, typo or formulation, do not hesitate to reach me on Twitter. I'll fix the problem as soon as possible. My english is probably not very good yet, but what computer science taught me is that we can't learn without practice.

Share

Vincent Dusautoir

Vincent Dusautoir

Whether you're looking for an expert for a cool digital project - or have a full-time job opportunity, you can easily reach me by email.

Available for work