Introduction
Let us continue building our chakra components clone using styled-components
& styled-system
. In this tutorial we will be cloning the Chakra UI Box
component.
- I would like you to first check the chakra docs for box.
- Then let us import
layout, color, typography, space, etc.
along withcompose
andsystem
from styled-system. For an introduction on how to use these check my introductory post - All the code for this tutorial can be found here under the atom-layout-box branch.
Prerequisite
Please check the previous post where we have setup the theme object and Provider. Also please check the Chakra Box Component code here. We will follow the atomic design for our folder structure. In this tutorial we will -
- Create a Box component.
- Create story for the Box component.
Setup
- First let us create a branch, from the main branch run -
git checkout -b atom-layout-box
- Install the following library, why do we need this you can read it here -
npm install @styled-system/should-forward-prop
- As the above library does not have a type package we add it typings to the typings.d.ts, also add typings for other files -
declare module "*.css" {
const content: { [className: string]: string };
export default content;
}
interface SvgrComponent
extends React.StatelessComponent<React.SVGAttributes<SVGElement>> {}
declare module "*.svg" {
const svgUrl: string;
const svgComponent: SvgrComponent;
export default svgUrl;
export { svgComponent as ReactComponent };
}
declare module "*.md";
declare module "@styled-system/should-forward-prop";
Components Folder Structure
- Create a
components
folder under the src folder. - Under
components
folder create 2 foldersatoms
andmolecules
. - And under the
atoms
folder create a new folder calledlayout
and underlayout
create a new folder calledbox
. - Also under layout create index.ts file.
- Under the
box
folder createindex.tsx
andbox.stories.tsx
files. So our folder structure stands like - src/components/atoms/layout/box.
Box Component
Under the src/components/atoms/layout/box/index.tsx
file we will create our first component using styled-components and styled-system.
- Let us first import the necessary libraries and styled-system utility functions like so -
import styled from "styled-components";
import {
compose,
space,
layout,
typography,
color,
shadow,
flex,
justifySelf,
alignSelf,
position,
border,
} from "styled-system";
import shouldForwardProp from "@styled-system/should-forward-prop";
- Now create our box component -
export const Box = styled.div.withConfig({
shouldForwardProp,
})`
box-sizing: border-box;
${compose(
space,
layout,
typography,
color,
shadow,
flex,
justifySelf,
alignSelf,
position,
border
)}
`;
There you go its done, we use compose if we are to use multiple utility functions for our component, else we would do
export const Box = styled.div`
box-sizing: border-box;
${color}
`;
Typing the Box Component
- If we were to use this component we would get type-errors because we have not created a type for our component yet. Now for each utility function that we imported from the styled-system library let us import the props, they have the same name as the utility-function,
SpaceProps for space, ColorProps for color and so on
. Replace your styled-system imports with the following -
import {
compose,
space,
layout,
typography,
color,
shadow,
flex,
justifySelf,
alignSelf,
position,
border,
SpaceProps,
LayoutProps,
TypographyProps,
ColorProps,
ShadowProps,
FlexProps,
JustifySelfProps,
AlignSelfProps,
PositionProps,
BorderProps,
} from "styled-system";
- Create a new type called BoxProps and use it like so -
export type BoxProps = SpaceProps &
LayoutProps &
TypographyProps &
ColorProps &
ShadowProps &
FlexProps &
JustifySelfProps &
AlignSelfProps &
PositionProps &
BorderProps &
React.ComponentPropsWithoutRef<"div"> & {
as?: React.ElementType;
};
export const Box = styled.div.withConfig({
shouldForwardProp,
})<BoxProps>`
box-sizing: border-box;
${compose(
space,
layout,
typography,
color,
shadow,
flex,
justifySelf,
alignSelf,
position,
border
)}
`;
- Note in the above code we are creating our
Box
as adiv
so we extend the props usingReact.ComponentPropsWithoutRef<"div">
, this will add OnClick, onHover, etc. div props. - We also added the as? polymorphic prop type for the styled-components.
Story
- With the above our
Box
component is completed, let us create a story. - Under the
src/components/atoms/layout/box/box.stories.tsx
file paste the following -
import * as React from "react";
import { Box, BoxProps } from ".";
export default {
title: "Atoms/Layout/Box",
};
export const Default = {
render: (args: BoxProps) => (
<Box bg="red500" color="white" p="md" {...args}>
Hello Component Library.
</Box>
),
};
- Now run
npm run storybook
, also check the autocompletion for props.
Look at it how cool it is, just like chakra. Also notice that I have for the bg passed value "red500", similarly for the p i.e. padding I passed "md". These values styled-system
is picking from the theme object keys color and spacing respectively. Please read about it in the docs. So it is very important that you have the right key names in the theme object. red500 and md here are called as tokens or design tokens.
Token AutoCompletion
One thing you might have noticed we don't get auto-completion for the values of styled props from our theme keys. No problem we can type our styled props, import AppTheme in the BoxComponent and make the following changes to the BoxProps -
- Let us have auto-completions for padding, bg and color under
src/components/atoms/layout/box/index.tsx
import -
import { AppTheme } from "../../../../theme";
- And for the BoxProps pass AppTheme to SpaceProps and ColorProps. The Styled System exposes these Generic types to which we pass our theme type -
export type BoxProps = SpaceProps<AppTheme> &
LayoutProps &
TypographyProps &
ColorProps<AppTheme> &
ShadowProps &
FlexProps &
JustifySelfProps &
AlignSelfProps &
PositionProps &
BorderProps &
React.ComponentPropsWithoutRef<"div"> & {
as?: React.ElementType;
};
The ColorProps will pick the type of the color key of our theme object, the space props will pick the space key and so on. For the layout we don't pass our AppTheme type because we don't have a corresponding size
key setup in our theme, you can add it if you want more on that here.
Now hit CTRL+SPACE and we get auto-completion for our design tokens
. But there are some caveats, like you now cannot pass any other value other than your theme values. So if you want to pass any other value first add it to your theme. For this project I am not going to type my tokens but you can.
Note to check the autocompletion remove the {...args} spreading.
Extending props with System
To extend Styled System for other CSS properties that aren't included in the library, use the system utility to create your own style functions - (https://styled-system.com/custom-props).
You might have noticed reading chakra ui docs that it has some neat handy utility props namely w - width, h - height, maxW - maxWidth, maxH - maxHeight, etc.
We don't get these utility props from styled-system but we can extend it. First import
system from styled-system
.Now copy the following code for the Box Component -
export const Box = styled.div.withConfig({
shouldForwardProp,
})<BoxProps>`
box-sizing: border-box;
${compose(
space,
layout,
typography,
color,
shadow,
flex,
justifySelf,
alignSelf,
position,
border
)}
${system({
w: {
property: "width",
},
h: {
property: "height",
},
maxW: {
property: "maxWidth",
},
maxH: {
property: "maxHeight",
},
basis: {
property: "flexBasis",
},
grow: {
property: "flexGrow",
},
shrink: {
property: "flexShrink",
},
marginStart: {
property: "marginInlineStart",
scale: "space",
},
marginEnd: {
property: "marginInlineEnd",
scale: "space",
},
})}
`;
Okay, let me explain. We added w, h, maxH, maxW to extend our system, these are not valid CSSProperty names therefore under the property key we mentioned width, height, maxHeight, maxWidth respectively.
Also we added some additional properties like basis, shrink, grow, so if we are using our
Box
inside aFlex
container we can just pass these handy props.What is interesting here is that for
marginStart
&marginEnd
we are passing thescale
key by this we tell styled-system which key to look into the theme if we pass a design token value say marginStart="md".It will look for the md value of the space key inside our theme
. Pretty cool.System is a core function of the library many other utility functions like the color we imported are nothing but made from system() - https://github.com/styled-system/styled-system/blob/master/packages/color/src/index.js
For other cool stuff that you can do with system please check my post here
Extending the BoxProps
- After extending our utility props we need to add these to the
BoxProps
type, for this let us create a new type called BoxOptions like so -
type BoxOptions = {
w?: LayoutProps["width"];
h?: LayoutProps["height"];
maxW?: LayoutProps["maxWidth"];
maxH?: LayoutProps["maxHeight"];
basis?: FlexBasisProps["flexBasis"];
grow?: FlexGrowProps["flexGrow"];
shrink?: FlexShrinkProps["flexShrink"];
marginStart?: SpaceProps["marginLeft"];
marginEnd?: SpaceProps["marginLeft"];
};
- Import the necessary types from
styled-system
. And extend BoxProps with BoxOptions -
export type BoxProps = SpaceProps &
LayoutProps &
TypographyProps &
ColorProps &
ShadowProps &
FlexProps &
JustifySelfProps &
AlignSelfProps &
PositionProps &
BorderProps &
BoxOptions &
React.ComponentPropsWithoutRef<"div"> & {
as?: React.ElementType;
};
- Now under stories play with these props. You might wonder like why we not used the FlexBasis, FlexShrink and FlexGrow props directly rather than adding them to system, well because we wanted a short form for these names. So we can use props with names like basis instead of flexBasis, shrink instead of flexShrink, grow instead of flexGrow this is inspired by Charka UI.
Playground Story
For Writing stories I like to follow this pattern of having 2 stories per component. One Default story
which is just a basic static component and other, Playground story
where we can use storybook's dynamic controls to pass props in real time and check the component.
- First open src/theme/spacing.ts and paste the following code
export function spacingOptions() {
const options = Object.keys(spacing);
const labels = Object.entries(spacing).reduce((acc, [key, value]) => {
acc[key] = `${key} (${value})`;
return acc;
}, {});
return { options, labels };
}
The code is self-explanatory it will loop over our theme options and give use an array of objects with the design token and it's value, you will see below how I use it.
Now under
src/components/atoms/layout/box/box.stories.tsx
paste the following code -
import * as React from "react";
import { spacingOptions } from "../../../../theme/spacing";
import { Box, BoxProps } from ".";
export default {
title: "Atoms/Layout/Box",
};
export const Playground = {
parameters: {
backgrounds: {
default: "grey",
},
},
argTypes: {
bg: {
name: "bg",
type: { name: "string", required: false },
defaultValue: "green800",
description: "Background Color CSS Prop for the component",
table: {
type: { summary: "string" },
defaultValue: { summary: "transparent" },
},
},
color: {
name: "color",
type: { name: "string", required: false },
defaultValue: "white",
description: "Color CSS Prop for the component",
table: {
type: { summary: "string" },
defaultValue: { summary: "black" },
},
},
p: {
name: "p",
type: { name: "string", required: false },
defaultValue: "md",
description: `Padding CSS prop for the Component shorthand for padding.
We also have pt, pb, pl, pr.`,
table: {
type: { summary: "string" },
defaultValue: { summary: "-" },
},
control: {
type: "select",
...spacingOptions(),
},
},
},
render: (args: BoxProps) => <Box {...args}>Hello</Box>,
};
export const Default = {
render: (args: BoxProps) => (
<Box bg="red500" color="white" p="md" {...args}>
Submit.
</Box>
),
};
- Now run
npm run storybook
check the Default and Playground stories. Under the Playground stories check the controls section play with the props, add more controls if you like.
Build the Library
- Under the
/layout
folder create an index.ts file and paste the following -
export * from "./box";
- Similarly under the
/atoms
folder create an index.ts file and paste the following -
export * from "./layout";
- In the main index.tsx file under the
/src
folder -
export * from "./provider";
export * from "./components/atoms";
Now
npm run build
.Under the folder
example/src/App.tsx
we can test ourBox
component. Copy paste the following code and runnpm run start
from theexample
directory.
import * as React from "react";
import { Box } from "chakra-ui-clone";
export function App() {
return (
<Box bg="red400" color="white" p="3rem" m="1rem">
Hello World
</Box>
);
}
Summary
There you go guys in this tutorial we created our first component and stories for it. You can find the code for this tutorial under the atom-layout-box branch here. In the next tutorial we will create Flex
component. Until next time PEACE.
Top comments (0)