Building a design system with dark mode using React, Typescript, scss, cva and Vite - Box & Flex Components
Tags: react, css, typescript, vite
Published: 2023-03-20 13:20:25

### Introduction
This is part three of our series on building a complete design system from scratch. In this tutorial we will create our first components `Box` and `Flex`. I would encourage you to play around with the deployed [storybook](http://yaldram-react-scss-chakra-ui.s3-website.ap-south-1.amazonaws.com/?path=/story/atoms-layout-flex--playground). All the code for this series is available on [GitHub](https://github.com/yaldram/react-scss-chakra-ui).

### Step One: Create the Box component
We already created a `Box` component in the first tutorial, now under `atoms/layout/box` create a `box.scss` - 
```scss
.box {
  box-sizing: border-box;
}
```
Now under `atoms/layout/box/index.tsx` paste the following - 
```tsx
import * as React from "react";
import { cva, cx } from "class-variance-authority";

import {
  bgColors,
  BgColorVariants,
  colors,
  ColorVariants,
  margin,
  MarginVariants,
  padding,
  PaddingVariants,
} from "../../../../cva-utils";

import "./box.scss";

const box = cva(["box"]);

export type BoxProps = ColorVariants &
  BgColorVariants &
  MarginVariants &
  PaddingVariants &
  React.ComponentPropsWithoutRef<"div">;

export const Box = React.forwardRef<HTMLDivElement, BoxProps>((props, ref) => {
  const {
    p,
    pt,
    pr,
    pb,
    pl,
    m,
    mt,
    mr,
    mb,
    ml,
    color,
    bg,
    className,
    children,
    ...delegated
  } = props;

  /**
   * Merge the utility classes
   */
  const boxClasses = cx(
    padding({ p, pt, pr, pb, pl }),
    margin({ m, mt, mr, mb, ml }),
    colors({ color }),
    bgColors({ bg }),
    box({ className })
  );

  return (
    <div className={boxClasses} ref={ref} {...delegated}>
      {children}
    </div>
  );
});
```
We imported all the cva variants that will become our utility props, merged them into one boxClasses using the cx function. Here is how we will use the `Box` component, all our theme tokens turned into utility props -   

```tsx
<Box bg="green400" color="blackAlpha200" p="md" m="sm">
  This is a Box component.
</Box>
```

Now create a `box.stories.tsx` and paste the following - 
```tsx
import * as React from "react";

import { StoryObj } from "@storybook/react";

import { Box, BoxProps } from ".";
import { spacingControls } from "../../../../cva-utils";

export default {
  title: "Atoms/Layout/Box",
};

const { spacingOptions, spacingLabels } = spacingControls();

export const Playground: StoryObj<BoxProps> = {
  args: {
    bg: "orange500",
    color: "black",
    p: "sm",
    m: "sm",
  },
  argTypes: {
    bg: {
      name: "bg",
      type: { name: "string", required: false },
      description: "Background Color CSS Prop for the component",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "transparent" },
      },
      control: {
        type: "text",
      },
    },
    color: {
      name: "color",
      type: { name: "string", required: false },
      description: "Color CSS Prop for the component",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "black" },
      },
      control: {
        type: "text",
      },
    },
    p: {
      name: "padding",
      type: { name: "string", required: false },
      options: spacingOptions,
      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",
        labels: spacingLabels,
      },
    },
    m: {
      name: "margin",
      type: { name: "string", required: false },
      options: spacingOptions,
      description: `Margin CSS prop for the Component shorthand for padding.
        We also have mt, mb, ml, mr.`,
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "-" },
      },
      control: {
        type: "select",
        labels: spacingLabels,
      },
    },
  },
  render: (args) => (
    <Box style={{ width: "100%" }} {...args}>
      Box Component
    </Box>
  ),
};

export const Default = () => (
  <Box style={{ width: "100%" }} p="lg" color="white" bg="teal700">
    Button
  </Box>
);
```
Now from the terminal run `yarn storybook` and check the output.

### Step Two: Create the Flex component
Things will get more clear, while building the `Flex` component. First under `atoms/layout/flex` folder create the `flex.scss` file and paste the following - 
```scss
/* base flex class */
.flex {
  display: flex;
}

/* flex direction classes */
.flex-row {
  flex-direction: row;
}
.flex-row-reverse {
  flex-direction: row-reverse;
}
.flex-col {
  flex-direction: column;
}
.flex-col-reverse {
  flex-direction: column-reverse;
}

/* justify classes */
.justify-start {
  justify-content: flex-start;
}
.justify-end {
  justify-content: flex-end;
}
.justify-center {
  justify-content: center;
}
.justify-between {
  justify-content: space-between;
}
.justify-around {
  justify-content: space-around;
}
.justify-evenly {
  justify-content: space-evenly;
}

/* align classes */
.align-start {
  align-items: flex-start;
}
.align-end {
  align-items: flex-end;
}
.align-center {
  align-items: center;
}
.align-baseline {
  align-items: baseline;
}
.align-stretch {
  align-items: stretch;
}

/* wrap classes */
.flex-wrap {
  flex-wrap: wrap;
}
.flex-wrap-reverse {
  flex-wrap: wrap-reverse;
}
.flex-nowrap {
  flex-wrap: nowrap;
}

/* class for spacer component */
.spacer {
  flex: 1;
  justify-self: stretch;
  align-self: stretch;
}
```
We basically added all the atomic classes needed for the `Flex` component. Now under `atoms/layouts/flex` folder create a new file `index.tsx` and paste the following - 
```tsx
import * as React from "react";
import { cva, cx, VariantProps } from "class-variance-authority";

import { flexGap, FlexGapVariants } from "../../../../cva-utils";
import { Box, BoxProps } from "../box";

import "./flex.scss";

const flex = cva(["flex"], {
  variants: {
    direction: {
      row: "flex-row",
      "row-reverse": "flex-row-reverse",
      col: "flex-col",
      "col-reverse": "flex-col-reverse",
    },
    justify: {
      start: "justify-start",
      end: "justify-end",
      center: "justify-center",
      between: "justify-between",
      around: "justify-around",
      evenly: "justify-evenly",
    },
    align: {
      start: "align-start",
      end: "align-end",
      center: "align-center",
      baseline: "align-baseline",
      stretch: "align-stretch",
    },
  },
  defaultVariants: {
    direction: "row",
  },
});

export type FlexProps = VariantProps<typeof flex> & FlexGapVariants & BoxProps;

export const Flex = React.forwardRef<HTMLDivElement, FlexProps>(
  (props, ref) => {
    const {
      direction,
      justify,
      align,
      gap,
      className,
      children,
      ...delegated
    } = props;

    const flexClasses = cx(
      flexGap({ gap }),
      flex({ direction, justify, align, className })
    );

    return (
      <Box ref={ref} className={flexClasses} {...delegated}>
        {children}
      </Box>
    );
  }
);

export interface SpacerProps extends BoxProps {}

export const Spacer = React.forwardRef<HTMLDivElement, SpacerProps>(
  (props, ref) => {
    const { children, ...delegated } = props;

    return (
      <Box ref={ref} className="spacer" {...delegated}>
        {children}
      </Box>
    );
  }
);
```
To the cva function we first passed the main `flex` class and created variants for our various utility props. We then create the `flexClasses` using the cx utility function, take a note we also used our `flexGap` cva function. Here is how we are going to use the `Flex` component, all our atomic classed turned into utility props - 
```tsx
<Flex direction="col" justify="center" align="start" gap="sm">
  <Box>First Component</Box>
  <Box>Second Component</Box>
</Flex>
```

Now create a new file called `flex.stories.tsx` -
```tsx
import * as React from "react";
import { StoryObj } from "@storybook/react";

import { Flex, FlexProps, Spacer } from ".";
import { spacingControls } from "../../../../cva-utils";

const { spacingOptions, spacingLabels } = spacingControls();

export default {
  title: "Atoms/Layout/Flex",
};

function Container(props: FlexProps) {
  const { children, ...delegated } = props;
  return (
    <Flex
      style={{
        minHeight: "100px",
        minWidth: "100px",
      }}
      justify="center"
      align="center"
      {...delegated}
    >
      {children}
    </Flex>
  );
}

export const Playground: StoryObj<FlexProps> = {
  args: {
    direction: "row",
    justify: "start",
    align: "stretch",
  },
  argTypes: {
    direction: {
      name: "direction",
      type: { name: "string", required: false },
      description: "Shorthand for flexDirection style prop",
      options: ["row", "row-reverse", "col", "col-reverse"],
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "row" },
      },
      control: {
        type: "select",
      },
    },
    justify: {
      name: "justify",
      type: { name: "string", required: false },
      options: ["start", "end", "center", "between", "around", "evenly"],
      description: "Shorthand for justifyContent style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "start" },
      },
      control: {
        type: "select",
      },
    },
    align: {
      name: "align",
      type: { name: "string", required: false },
      options: ["start", "end", "center", "baseline", "stretch"],
      description: "Shorthand for alignItems style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "stretch" },
      },
      control: {
        type: "select",
      },
    },
    gap: {
      name: "gap",
      type: { name: "string", required: false },
      options: spacingOptions,
      description: "Shorthand for flexGap style prop",
      table: {
        type: { summary: "string" },
      },
      control: {
        type: "select",
        labels: spacingLabels,
      },
    },
  },
  render: (args) => (
    <Flex style={{ width: "100%" }} bg="blue100" color="white" p="md" {...args}>
      <Container bg="green500">Box 1</Container>
      <Container bg="blue500">Box 2</Container>
      <Container bg="orange500">Box 3</Container>
    </Flex>
  ),
};

export const FlexSpacer = {
  args: {
    direction: "row",
  },
  argTypes: {
    direction: {
      name: "direction",
      type: { name: "string", required: false },
      options: ["row", "row-reverse", "col", "col-reverse"],
      description: "Shorthand for flexDirection style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "row" },
      },
      control: {
        type: "select",
      },
    },
  },
  render: (args: FlexProps) => (
    <Flex
      style={{ width: "100%", height: "80vh" }}
      color="white"
      bg="blackAlpha200"
      p="xs"
      {...args}
    >
      <Container p="md" bg="red400">
        Box 1
      </Container>
      <Spacer />
      <Container p="md" bg="green400">
        Box 2
      </Container>
    </Flex>
  ),
};

export const Stack: StoryObj<FlexProps> = {
  args: {
    direction: "row",
    gap: "md",
  },
  argTypes: {
    direction: {
      name: "direction",
      type: { name: "string", required: false },
      description: "Shorthand for flexDirection style prop",
      options: ["row", "row-reverse", "col", "col-reverse"],
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "row" },
      },
      control: {
        type: "select",
      },
    },
    align: {
      name: "align",
      type: { name: "string", required: false },
      options: ["start", "end", "center", "baseline", "stretch"],
      description: "Shorthand for alignItems style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "stretch" },
      },
      control: {
        type: "select",
      },
    },
    gap: {
      name: "gap",
      type: { name: "string", required: false },
      options: spacingOptions,
      description: "Shorthand for flexGap style prop",
      table: {
        type: { summary: "string" },
      },
      control: {
        type: "select",
        labels: spacingLabels,
      },
    },
  },
  render: (args) => (
    <Flex
      style={{ width: "100%", minHeight: "100vh" }}
      color="white"
      bg="blue100"
      p="md"
      {...args}
    >
      <Container p="md" bg="yellow500">
        Box 1
      </Container>
      <Container p="md" bg="red500">
        Box 2
      </Container>
      <Container p="md" bg="teal500">
        Box 3
      </Container>
    </Flex>
  ),
};
```
From the terminal run `yarn storybook`, I would encourage you to play around with the components and you will understand the code much better. Also let me know if you have any queries.

From `atoms/layouts/index.ts` export these components - 
```ts
export * from "./box";
export * from "./flex";
```

Finally, under `atoms/index.ts` paste - 
```ts
export * from "./layouts";
```

### Conclusion
In this tutorial we created the `Box` & `Flex` components. All the code for this tutorial can be found [here](https://github.com/yaldram/react-scss-chakra-ui). In the next tutorial we will create our first theme able component `Badge` with both light and dark modes. Until next time PEACE.