Build Chakra UI Icon component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-04 07:53:37

### Introduction

Let us continue building our chakra components using `styled-components` & `styled-system`. In this tutorial we will be cloning the Chakra UI `Icon` component.

- I would like you to first check the chakra docs for [icon](https://chakra-ui.com/docs/media-and-icons/icon).
- We will **compose** (extend) our `Box` component to create the `Icon` component.
- All the code for this tutorial can be found under the **atom-icons** branch [here](https://github.com/yaldram/chakra-ui-clone).

### Prerequisite

Please check the Chakra Icon Component code [here](https://github.com/chakra-ui/chakra-ui/tree/main/packages/icon/src).

In this tutorial we will -

- Create an `Icon` component.
- A helper function called `createIcon`.
- We will use `Icon` and `createIcon` to create some Icons :).

### Setup

- First let us create a branch, from the main branch run -

```bash
git checkout -b atom-icons
```

- Create a new folder under `components/atoms`, name it `icon`.

- Under `components/atom/icon` folder create 3 files `index.ts`, `icon.tsx` and `create-icon.tsx`.

- So our folder structure stands like - **src/components/atoms/icon**.

### Icon Component

- Under `icon/icon.tsx` paste the following code -

```tsx
import * as React from "react";

import { Box, BoxProps } from "../layout";

const fallbackIcon = {
  path: (
    <g stroke="currentColor" strokeWidth="1.5">
      <path
        strokeLinecap="round"
        fill="none"
        d="M9,9a3,3,0,1,1,4,2.829,1.5,1.5,0,0,0-1,1.415V14.25"
      />
      <path
        fill="currentColor"
        strokeLinecap="round"
        d="M12,17.25a.375.375,0,1,0,.375.375A.375.375,0,0,0,12,17.25h0"
      />
      <circle fill="none" strokeMiterlimit="10" cx="12" cy="12" r="11.25" />
    </g>
  ),
  viewBox: "0 0 24 24",
};

export interface IconProps
  extends BoxProps,
    Omit<React.SVGAttributes<SVGElement>, keyof BoxProps> {}

export const Icon = React.forwardRef<SVGElement, IconProps>((props, ref) => {
  const {
    as: element,
    viewBox = fallbackIcon.viewBox,
    color = "currentColor",
    focusable = false,
    children,
    ...delegated
  } = props;

  const sharedProps = {
    width: "1em",
    height: "1em",
    display: "inline-block",
    lineHeight: "1em",
    flexShrink: 0,
    color,
    viewBox,
    ref,
    focusable,
  };

  if (element && typeof element !== "string") {
    return <Box ref={ref} as={element} {...sharedProps} {...delegated} />;
  }

  const path = (children ?? fallbackIcon.path) as React.ReactNode;

  return (
    <Box
      // @ts-ignore
      ref={ref}
      as="svg"
      verticalAlign="middle"
      {...sharedProps}
      {...delegated}
    >
      {path}
    </Box>
  );
});
```

- Let us consider `IconProps`, it extends BoxProps. There were some conflicting types if we were to extend the SVGElement and BoxProps as a result we need to Omit the BoxProps Keys.

- As you might have noticed chakra UI lets you pass custom icons like -

```tsx
<Icon as={MdGroupWork} w="40px" h="40px" color="red500" />
```

- We have mimicked, it here -

```tsx
if (element && typeof element !== "string") {
  return <Box ref={ref} as={element} {...sharedProps} {...delegated} />;
}
```

- For the normal Icon component usage like -

```tsx
<Icon viewBox="0 0 200 200" color="red.500">
  <path
    fill="currentColor"
    d="M 100, 100 m -75, 0 a 75,75 0 1,0 150,0 a 75,75 0 1,0 -150,0"
  />
</Icon>
```

- We skip the if part, note the path variable we are using a fallback icon if nothing is passed -

```tsx
return (
  <Box
    // @ts-ignore
    ref={ref}
    as="svg"
    verticalAlign="middle"
    {...sharedProps}
    {...delegated}
  >
    {path}
  </Box>
);
```

- I have added ts-ignore because of the ref type mismatch as the Box expects a Div type and we are passing a SVG type. This is one of the many short comings of using styled-component's polymorphic `as` props with typescript.

- `height` & `width` is set to 1em each so that by default it's dimensions will be proportional to it's parent's.

### createIcon Function

- Under `atoms/icon/create-icon.tsx` paste the following -

```tsx
import * as React from "react";

import { Icon, IconProps } from "./icon";

interface CreateIconOptions {
  viewBox?: string;
  path?: React.ReactElement | React.ReactElement[];
  d?: string;
  defaultProps?: IconProps;
}

export function createIcon(options: CreateIconOptions) {
  const {
    viewBox = "0 0 24 24",
    d: pathDefinition,
    path,
    defaultProps = {},
  } = options;

  const Component = React.forwardRef<SVGElement, IconProps>((props, ref) => (
    <Icon ref={ref} viewBox={viewBox} {...defaultProps} {...props}>
      {path ?? <path fill="currentColor" d={pathDefinition} />}
    </Icon>
  ));

  return Component;
}
```

- This is pretty slick, it just returns the Icon component, but gives you some really flexible options -

  - `path` - The `svg` path or group element. Just pass in the path you have and you get an Icon.
  - `d` - If icon has a single path, simply copy the path's `d` attribute.
  - `defaultProps` - Also you can pass props to the underlying icon component.

- Under `atoms/icon/index.ts` paste the following -

```ts
export * from "./icon";
export * from "./create-icon";
```

### Icons

- Let us now use `Icon` and `createIcon` to create some icons, you will understand them better. Check chakra's icons code [here](https://github.com/chakra-ui/chakra-ui/tree/main/packages/icons/src).

- First under `atoms/components` create a new folder called `icons`, under this new folder create an `index.tsx` file and paste the following code -

```tsx
/* eslint-disable max-len */
import * as React from "react";

import { Icon, IconProps } from "../icon/icon";
import { createIcon } from "../icon/create-icon";

export const CheckIcon = createIcon({
  d: "M12,0A12,12,0,1,0,24,12,12.014,12.014,0,0,0,12,0Zm6.927,8.2-6.845,9.289a1.011,1.011,0,0,1-1.43.188L5.764,13.769a1,1,0,1,1,1.25-1.562l4.076,3.261,6.227-8.451A1,1,0,1,1,18.927,8.2Z",
});

export const InfoIcon = createIcon({
  d: "M12,0A12,12,0,1,0,24,12,12.013,12.013,0,0,0,12,0Zm.25,5a1.5,1.5,0,1,1-1.5,1.5A1.5,1.5,0,0,1,12.25,5ZM14.5,18.5h-4a1,1,0,0,1,0-2h.75a.25.25,0,0,0,.25-.25v-4.5a.25.25,0,0,0-.25-.25H10.5a1,1,0,0,1,0-2h1a2,2,0,0,1,2,2v4.75a.25.25,0,0,0,.25.25h.75a1,1,0,1,1,0,2Z",
});

export const WarningIcon = createIcon({
  d: "M11.983,0a12.206,12.206,0,0,0-8.51,3.653A11.8,11.8,0,0,0,0,12.207,11.779,11.779,0,0,0,11.8,24h.214A12.111,12.111,0,0,0,24,11.791h0A11.766,11.766,0,0,0,11.983,0ZM10.5,16.542a1.476,1.476,0,0,1,1.449-1.53h.027a1.527,1.527,0,0,1,1.523,1.47,1.475,1.475,0,0,1-1.449,1.53h-.027A1.529,1.529,0,0,1,10.5,16.542ZM11,12.5v-6a1,1,0,0,1,2,0v6a1,1,0,1,1-2,0Z",
});

export const WarningTwoIcon = createIcon({
  d: "M23.119,20,13.772,2.15h0a2,2,0,0,0-3.543,0L.881,20a2,2,0,0,0,1.772,2.928H21.347A2,2,0,0,0,23.119,20ZM11,8.423a1,1,0,0,1,2,0v6a1,1,0,1,1-2,0Zm1.05,11.51h-.028a1.528,1.528,0,0,1-1.522-1.47,1.476,1.476,0,0,1,1.448-1.53h.028A1.527,1.527,0,0,1,13.5,18.4,1.475,1.475,0,0,1,12.05,19.933Z",
});

export const CloseIcon = createIcon({
  d: "M.439,21.44a1.5,1.5,0,0,0,2.122,2.121L11.823,14.3a.25.25,0,0,1,.354,0l9.262,9.263a1.5,1.5,0,1,0,2.122-2.121L14.3,12.177a.25.25,0,0,1,0-.354l9.263-9.262A1.5,1.5,0,0,0,21.439.44L12.177,9.7a.25.25,0,0,1-.354,0L2.561.44A1.5,1.5,0,0,0,.439,2.561L9.7,11.823a.25.25,0,0,1,0,.354Z",
});

export const SearchIcon = createIcon({
  d: "M23.384,21.619,16.855,15.09a9.284,9.284,0,1,0-1.768,1.768l6.529,6.529a1.266,1.266,0,0,0,1.768,0A1.251,1.251,0,0,0,23.384,21.619ZM2.75,9.5a6.75,6.75,0,1,1,6.75,6.75A6.758,6.758,0,0,1,2.75,9.5Z",
});

export const Search2Icon = createIcon({
  d: "M23.414,20.591l-4.645-4.645a10.256,10.256,0,1,0-2.828,2.829l4.645,4.644a2.025,2.025,0,0,0,2.828,0A2,2,0,0,0,23.414,20.591ZM10.25,3.005A7.25,7.25,0,1,1,3,10.255,7.258,7.258,0,0,1,10.25,3.005Z",
});

export const PhoneIcon = createIcon({
  d: "M2.20731,0.0127209 C2.1105,-0.0066419 1.99432,-0.00664663 1.91687,0.032079 C0.871279,0.438698 0.212942,1.92964 0.0580392,2.95587 C-0.426031,6.28627 2.20731,9.17133 4.62766,11.0689 C6.77694,12.7534 10.9012,15.5223 13.3409,12.8503 C13.6507,12.5211 14.0186,12.037 13.9993,11.553 C13.9412,10.7397 13.186,10.1588 12.6051,9.71349 C12.1598,9.38432 11.2304,8.47427 10.6495,8.49363 C10.1267,8.51299 9.79754,9.05515 9.46837,9.38432 L8.88748,9.96521 C8.79067,10.062 7.55145,9.24878 7.41591,9.15197 C6.91248,8.8228 6.4284,8.45491 6.00242,8.04829 C5.57644,7.64167 5.18919,7.19632 4.86002,6.73161 C4.7632,6.59607 3.96933,5.41495 4.04678,5.31813 C4.04678,5.31813 4.72448,4.58234 4.91811,4.2919 C5.32473,3.67229 5.63453,3.18822 5.16982,2.45243 C4.99556,2.18135 4.78257,1.96836 4.55021,1.73601 C4.14359,1.34875 3.73698,0.942131 3.27227,0.612963 C3.02055,0.419335 2.59457,0.0708094 2.20731,0.0127209 Z",
  viewBox: "0 0 14 14",
});

export const EmailIcon = createIcon({
  path: (
    <g fill="currentColor">
      <path d="M11.114,14.556a1.252,1.252,0,0,0,1.768,0L22.568,4.87a.5.5,0,0,0-.281-.849A1.966,1.966,0,0,0,22,4H2a1.966,1.966,0,0,0-.289.021.5.5,0,0,0-.281.849Z" />
      <path d="M23.888,5.832a.182.182,0,0,0-.2.039l-6.2,6.2a.251.251,0,0,0,0,.354l5.043,5.043a.75.75,0,1,1-1.06,1.061l-5.043-5.043a.25.25,0,0,0-.354,0l-2.129,2.129a2.75,2.75,0,0,1-3.888,0L7.926,13.488a.251.251,0,0,0-.354,0L2.529,18.531a.75.75,0,0,1-1.06-1.061l5.043-5.043a.251.251,0,0,0,0-.354l-6.2-6.2a.18.18,0,0,0-.2-.039A.182.182,0,0,0,0,6V18a2,2,0,0,0,2,2H22a2,2,0,0,0,2-2V6A.181.181,0,0,0,23.888,5.832Z" />
    </g>
  ),
});

export const ArrowForwardIcon = createIcon({
  d: "M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8z",
});

export const AvatarFallback: React.FC<IconProps> = (props) => {
  return (
    <Icon viewBox="0 0 128 128" color="#fff" w="100%" h="100%" {...props}>
      <path
        fill="currentColor"
        d="M103,102.1388 C93.094,111.92 79.3504,118 64.1638,118 C48.8056,118 34.9294,111.768 25,101.7892 L25,95.2 C25,86.8096 31.981,80 40.6,80 L87.4,80 C96.019,80 103,86.8096 103,95.2 L103,102.1388 Z"
      />
      <path
        fill="currentColor"
        d="M63.9961647,24 C51.2938136,24 41,34.2938136 41,46.9961647 C41,59.7061864 51.2938136,70 63.9961647,70 C76.6985159,70 87,59.7061864 87,46.9961647 C87,34.2938136 76.6985159,24 63.9961647,24"
      />
    </Icon>
  );
};

export const AiOutlineUser: React.FC<IconProps> = (props) => {
  return (
    <Icon
      stroke="currentColor"
      stroke-width="0"
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 1024 1024"
      {...props}
    >
      <path d="M858.5 763.6a374 374 0 0 0-80.6-119.5 375.63 375.63 0 0 0-119.5-80.6c-.4-.2-.8-.3-1.2-.5C719.5 518 760 444.7 760 362c0-137-111-248-248-248S264 225 264 362c0 82.7 40.5 156 102.8 201.1-.4.2-.8.3-1.2.5-44.8 18.9-85 46-119.5 80.6a375.63 375.63 0 0 0-80.6 119.5A371.7 371.7 0 0 0 136 901.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 0 0 8-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-45.9 0-89.1-17.9-121.6-50.4S340 407.9 340 362c0-45.9 17.9-89.1 50.4-121.6S466.1 190 512 190s89.1 17.9 121.6 50.4S684 316.1 684 362c0 45.9-17.9 89.1-50.4 121.6S557.9 534 512 534z" />
    </Icon>
  );
};
```

### Build the Library

- Under the `/atom/index.ts` file and paste the following -

```tsx
export * from "./layout";
export * from "./typography";
export * from "./feedback";
export * from "./icon";
export * from "./icons";
```

- Now `npm run build`.

- Also to test whether our Icon component is working with external Icon packages under the example folder -

```bash
npm install react-icons
```

- Under the folder `example/src/App.tsx` we can test our `Icon` component. Copy paste the following code and run `npm run start` from the `example` directory.

```tsx
import * as React from "react";
import { Stack, Icon, CheckIcon, createIcon, IconProps } from "chakra-ui-clone";
import { MdSettings, MdGroupWork } from "react-icons/md";

const BellIcon = createIcon({
  d: "M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z",
});

const TimeIcon = (props: IconProps) => (
  <Icon {...props}>
    <g fill="currentColor">
      <path d="M12,0A12,12,0,1,0,24,12,12.014,12.014,0,0,0,12,0Zm0,22A10,10,0,1,1,22,12,10.011,10.011,0,0,1,12,22Z" />
      <path d="M17.134,15.81,12.5,11.561V6.5a1,1,0,0,0-2,0V12a1,1,0,0,0,.324.738l4.959,4.545a1.01,1.01,0,0,0,1.413-.061A1,1,0,0,0,17.134,15.81Z" />
    </g>
  </Icon>
);

export function App() {
  return (
    <Stack m="2rem" spacing="xl">
      <Icon as={MdSettings} />
      <Icon as={MdGroupWork} w="40px" h="40px" color="red500" />
      <CheckIcon />
      <CheckIcon color="green500" size="35px" />
      <BellIcon />
      <BellIcon size="50px" color="red500" />
      <TimeIcon size="30px" color="blue500" />
      <Icon />
    </Stack>
  );
}
```

**Let me appreciate the simplicity and the perfection by which these components are build you can check the above code it is so simple, easy, extensible. All thanks to Chakra UI**.

### Summary

There you go guys in this tutorial we created `Icon` component just like chakra ui. You can find the code for this tutorial under the **atom-icons** branch [here](https://github.com/yaldram/chakra-ui-clone). In the next tutorial we will create `Button` component. Until next time PEACE.