DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Build Chakra UI Icon component using react, typescript, styled-components and styled-system

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.
  • 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.

Prerequisite

Please check the Chakra Icon Component code here.

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 -
git checkout -b atom-icons
Enter fullscreen mode Exit fullscreen mode
  • 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 -
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>
  );
});
Enter fullscreen mode Exit fullscreen mode
  • 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 -

<Icon as={MdGroupWork} w="40px" h="40px" color="red500" />
Enter fullscreen mode Exit fullscreen mode
  • We have mimicked, it here -
if (element && typeof element !== "string") {
  return <Box ref={ref} as={element} {...sharedProps} {...delegated} />;
}
Enter fullscreen mode Exit fullscreen mode
  • For the normal Icon component usage like -
<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>
Enter fullscreen mode Exit fullscreen mode
  • We skip the if part, note the path variable we are using a fallback icon if nothing is passed -
return (
  <Box
    // @ts-ignore
    ref={ref}
    as="svg"
    verticalAlign="middle"
    {...sharedProps}
    {...delegated}
  >
    {path}
  </Box>
);
Enter fullscreen mode Exit fullscreen mode
  • 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 -
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;
}
Enter fullscreen mode Exit fullscreen mode
  • 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 -

export * from "./icon";
export * from "./create-icon";
Enter fullscreen mode Exit fullscreen mode

Icons

  • Let us now use Icon and createIcon to create some icons, you will understand them better. Check chakra's icons code here.

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

/* 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Build the Library

  • Under the /atom/index.ts file and paste the following -
export * from "./layout";
export * from "./typography";
export * from "./feedback";
export * from "./icon";
export * from "./icons";
Enter fullscreen mode Exit fullscreen mode
  • Now npm run build.

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

npm install react-icons
Enter fullscreen mode Exit fullscreen mode
  • 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.
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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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. In the next tutorial we will create Button component. Until next time PEACE.

Discussion (0)