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
Boxcomponent to create theIconcomponent. - 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
Iconcomponent. - A helper function called
createIcon. - We will use
IconandcreateIconto 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/iconfolder create 3 filesindex.ts,icon.tsxandcreate-icon.tsx.So our folder structure stands like - src/components/atoms/icon.
Icon Component
- Under
icon/icon.tsxpaste 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
asprops with typescript.height&widthis 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.tsxpaste 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- Thesvgpath 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'sdattribute. -
defaultProps- Also you can pass props to the underlying icon component.
-
Under
atoms/icon/index.tspaste the following -
export * from "./icon";
export * from "./create-icon";
Icons
Let us now use
IconandcreateIconto create some icons, you will understand them better. Check chakra's icons code here.First under
atoms/componentscreate a new folder calledicons, under this new folder create anindex.tsxfile 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.tsfile 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.tsxwe can test ourIconcomponent. Copy paste the following code and runnpm run startfrom theexampledirectory.
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)