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 theIcon
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
andcreateIcon
to create some Icons :).
Setup
- First let us create a branch, from the main branch run -
git checkout -b atom-icons
Create a new folder under
components/atoms
, name iticon
.Under
components/atom/icon
folder create 3 filesindex.ts
,icon.tsx
andcreate-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>
);
});
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" />
- We have mimicked, it here -
if (element && typeof element !== "string") {
return <Box ref={ref} as={element} {...sharedProps} {...delegated} />;
}
- 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>
- 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>
);
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;
}
-
This is pretty slick, it just returns the Icon component, but gives you some really flexible options -
-
path
- Thesvg
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'sd
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";
Icons
Let us now use
Icon
andcreateIcon
to create some icons, you will understand them better. Check chakra's icons code here.First under
atoms/components
create a new folder calledicons
, under this new folder create anindex.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>
);
};
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";
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
- Under the folder
example/src/App.tsx
we can test ourIcon
component. Copy paste the following code and runnpm run start
from theexample
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>
);
}
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.
Top comments (0)