Chakra UI Box component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-01 13:49:47

### 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](https://chakra-ui.com/docs/layout/box).
- Then let us import `layout, color, typography, space, etc.` along with `compose` and `system` from styled-system. For an introduction on how to use these check my [introductory post](https://dev.to/yaldram/working-with-styled-system-using-typescript-4mkj)
- All the code for this tutorial can be found [here](https://github.com/yaldram/chakra-ui-clone) 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](https://github.com/chakra-ui/chakra-ui/blob/main/packages/layout/src/box.tsx). 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 -

```bash
git checkout -b atom-layout-box
```

- Install the following library, why do we need this you can read it [here](https://styled-system.com/guides/removing-props-from-html/) -

```bash
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 -

```ts
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 folders `atoms` and `molecules`.
- And under the `atoms` folder create a new folder called `layout` and under `layout` create a new folder called `box`.
- Also under layout create index.ts file.
- Under the `box` folder create `index.tsx` and `box.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 -

```tsx
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 -

```tsx
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

```tsx
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 -

```tsx
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 -

```tsx
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 a `div` so we extend the props using `React.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 -

```tsx
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.

  ![autocompletion](https://res.cloudinary.com/dkl3acuus/image/upload/v1629794625/posts/chakra-ui-clone/autocompletion_q2wd3w.png)

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](https://styled-system.com/api). 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 -

```tsx
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 -

```tsx
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](https://styled-system.com/api/#layout).

![tokens-autocompletion](https://res.cloudinary.com/dkl3acuus/image/upload/v1629795630/posts/chakra-ui-clone/token-autocompletion_hl3pzo.png)

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 -

```tsx
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 a `Flex` container we can just pass these handy props.

- What is interesting here is that for `marginStart` & `marginEnd` we are passing the `scale` 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](https://dev.to/yaldram/working-with-styled-system-using-typescript-4mkj)

### 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 -

```tsx
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 -

```tsx
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

```ts
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 -

```tsx
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.

![storybook-controls](https://res.cloudinary.com/dkl3acuus/image/upload/v1629797524/posts/chakra-ui-clone/storybook-controls_g1rkov.png)

### Build the Library

- Under the `/layout` folder create an index.ts file and paste the following -

```tsx
export * from "./box";
```

- Similarly under the `/atoms` folder create an index.ts file and paste the following -

```tsx
export * from "./layout";
```

- In the main index.tsx file under the `/src` folder -

```tsx
export * from "./provider";

export * from "./components/atoms";
```

- Now `npm run build`.

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

```tsx
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](https://github.com/yaldram/chakra-ui-clone). In the next tutorial we will create `Flex` component. Until next time PEACE.