DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on • Updated on

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

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 with compose and system 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
Enter fullscreen mode Exit fullscreen mode
  • Install the following library, why do we need this you can read it here -
npm install @styled-system/should-forward-prop
Enter fullscreen mode Exit fullscreen mode
  • 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";
Enter fullscreen mode Exit fullscreen mode

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 -
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";
Enter fullscreen mode Exit fullscreen mode
  • 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
  )}
`;
Enter fullscreen mode Exit fullscreen mode

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}
`;
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode
  • 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
  )}
`;
Enter fullscreen mode Exit fullscreen mode
  • 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 -
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>
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • Now run npm run storybook, also check the autocompletion for props.

autocompletion

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";
Enter fullscreen mode Exit fullscreen mode
  • 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;
  };
Enter fullscreen mode Exit fullscreen mode

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.

tokens-autocompletion

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",
    },
  })}
`;
Enter fullscreen mode Exit fullscreen mode
  • 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

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"];
};
Enter fullscreen mode Exit fullscreen mode
  • 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;
  };
Enter fullscreen mode Exit fullscreen mode
  • 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 };
}
Enter fullscreen mode Exit fullscreen mode
  • 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>
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • 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

Build the Library

  • Under the /layout folder create an index.ts file and paste the following -
export * from "./box";
Enter fullscreen mode Exit fullscreen mode
  • Similarly under the /atoms folder create an index.ts file and paste the following -
export * from "./layout";
Enter fullscreen mode Exit fullscreen mode
  • In the main index.tsx file under the /src folder -
export * from "./provider";

export * from "./components/atoms";
Enter fullscreen mode Exit fullscreen mode
  • 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.

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

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.

Discussion (0)