Build Chakra UI Stack component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-02 07:45:39

### Introduction

Let us continue building our chakra components using `styled-components` & `styled-system`. In this tutorial we will be cloning the Chakra UI `Stack` component.

- I would like you to first check the chakra docs for [stack](https://chakra-ui.com/docs/layout/stack).
- We will **compose** (extend) our `Flex` component to create the `Stack` component and further extend the `Stack` component to create a `HStack` and `VStack` components.
- All the code for this tutorial can be found under the **atom-layout-stack** branch [here](https://github.com/yaldram/chakra-ui-clone).

### Prerequisite

Please check the previous post where we have completed the Flex Component. Also please check the Chakra Stack Component code [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/layout/src/stack.tsx).

In this tutorial we will be using `variants` from `styled-system`, therefore I would advise you to check check my [introductory post](https://dev.to/yaldram/working-with-styled-system-using-typescript-4mkj).

Check the `variant` function [docs](https://styled-system.com/variants).

In this tutorial we will -

- Create a Stack component.
- Create HStack and VStack components.
- Create story for the Stack component.
- The chakra Stack component can also take in a Divider component we won't be implementing that functionality, we will just focus on the Base Stack Component.

### Setup

- First let us create a branch, from the main branch run -

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

- Under the `components/atoms/layout` folder create a new folder called stack. Under stack folder create 2 files `index.tsx` and `stack.stories.tsx`.

- So our folder structure stands like - **src/components/atoms/layout/stack**.

### Stack Component

- First we will create a util function `getValidChildren`, it loops over the children and return only valid React Elements. Under src create a new folder called `utils`. Under utils create 2 files index.ts and dom.ts. Under dom.ts paste the following -

```ts
import * as React from "react";

export function getValidChildren(children: React.ReactNode) {
  return React.Children.toArray(children).filter((child) =>
    React.isValidElement(child)
  ) as React.ReactElement[];
}
```

- Under utils/index.ts paste the following -

```ts
export * from "./dom";
```

- Under `src/components/atoms/layout/stack/index.tsx` lets import the necessary stuff -

```tsx
import * as React from "react";
import styled from "styled-components";
import { variant, ResponsiveValue } from "styled-system";

import { getValidChildren } from "../../../../utils";
import { Flex, FlexProps } from "../flex";
```

- The Chakra stack component takes in 2 important props namely `direction` for the stack and `spacing` for the elements. Therefore what I did was, I used the `variant` function for the `direction` prop like so -

```tsx
type StackVariants = "row" | "column" | "row-reverse" | "column-reverse";

type StackOptions = {
  spacing: string;
  direction: ResponsiveValue<StackVariants>;
};

type BaseStackProps = StackOptions & FlexProps;

const selector = "& > *:not(style) ~ *:not(style)";

const stackVariants = (spacing: string) => ({
  row: {
    flexDirection: "row",
    [selector]: {
      margin: 0,
      marginLeft: spacing,
    },
  },
  column: {
    flexDirection: "column",
    [selector]: {
      margin: 0,
      marginTop: spacing,
    },
  },
  "row-reverse": {
    flexDirection: "row-reverse",
    [selector]: {
      margin: 0,
      marginRight: spacing,
    },
  },
  "column-reverse": {
    flexDirection: "column-reverse",
    [selector]: {
      margin: 0,
      marginBottom: spacing,
    },
  },
});

const BaseStack = styled(Flex)<BaseStackProps>`
  & > * {
    margin: 0;
  }

  ${(props) =>
    variant({
      prop: "direction",
      variants: stackVariants(props.spacing),
    })}
`;
```

- First I created the type `StackVariants`, for the direction prop. Then I created the `BaseStackProps` type that extend the `FlexProps` & `StackOptions`.

- Note for `StackOptions`, for the direction field we used generic type ResponsiveValue, this will allow us to pass responsive props to our component like below, more on that [here](https://styled-system.com/responsive-styles).

```tsx
<Stack direction={["row", "column"]}></Stack>
```

- Now let us create the `Stack` component -

```tsx
export interface StackProps
  extends Omit<FlexProps, "direction">,
    Partial<StackOptions> {}

export const Stack = React.forwardRef<HTMLDivElement, StackProps>(
  (props, ref) => {
    const { direction = "row", spacing = "md", children, ...delegated } = props;

    return (
      <BaseStack
        direction={direction}
        spacing={spacing}
        ref={ref}
        {...delegated}
      >
        {getValidChildren(children)}
      </BaseStack>
    );
  }
);
```

- Note a few things we have Partial<StackOptions>, meaning these are not required props, therefore we have destructed these props and passed the default values. Second take a look at the default value for spacing it is `md` which is our token not an actual value like "2rem, 20px, etc". **This means we can directly refer to our token values from the variant function call using styled-system, which is awesome**.

### HStack Component

The `HStack` component stands for horizontal stack. It is used to add spacing between elements in horizontal direction, and also centers them vertically. So we will omit the vertical values for the direction prop, and have alignItems property equals center, like so -

```tsx
export interface HStackProps extends Omit<StackProps, "direction"> {
  direction?: "row" | "row-reverse";
}

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

    return (
      <Stack direction="row" align="center" ref={ref} {...delegated}>
        {children}
      </Stack>
    );
  }
);
```

### VStack Component

The `VStack` component stands for vertical stack. It is Used to add spacing between elements in vertical direction only, and centers them horizontally. So we will omit the horizontal values for the direction prop, and have alignItems property equals center, like so -

```tsx
export interface VStackProps extends Omit<StackProps, "direction"> {
  direction?: "column" | "column-reverse";
}

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

    return (
      <Stack direction="column" align="center" ref={ref} {...delegated}>
        {children}
      </Stack>
    );
  }
);
```

### Story

- With the above our `Stack` components are completed, let us create a story.
- Under the `src/components/atoms/layout/stack/stack.stories.tsx` file we add the below story code.
- We will create 3 stories - Playground, horizontalStack, verticalStack.

```tsx
import * as React from "react";

import { spacingOptions } from "../../../../theme/spacing";
import { Box } from "../box";
import { Stack, StackProps, HStack, HStackProps, VStack, VStackProps } from ".";

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

const spacingSelect = {
  name: "spacing",
  type: { name: "string", required: false },
  defaultValue: "lg",
  description: "The gap between stack items.",
  table: {
    type: { summary: "string" },
    defaultValue: { summary: "md" },
  },
  control: {
    type: "select",
    ...spacingOptions(),
  },
};

const alignSelect = {
  name: "align",
  type: { name: "string", required: false },
  defaultValue: "center",
  description: "Shorthand for alignItems style prop",
  table: {
    type: { summary: "string" },
    defaultValue: { summary: "center" },
  },
  control: {
    type: "select",
    options: [
      "stretch",
      "center",
      "flex-start",
      "flex-end",
      "baseline",
      "initial",
      "inherit",
    ],
  },
};

export const Playground = {
  argTypes: {
    direction: {
      name: "direction",
      type: { name: "string", required: false },
      defaultValue: "row",
      description: "Shorthand for flexDirection style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "row" },
      },
      control: {
        type: "select",
        options: ["row", "row-reverse", "column", "column-reverse"],
      },
    },
    spacing: spacingSelect,
  },
  render: (args: StackProps) => (
    <Stack {...args}>
      <Box p="md" h="40px" bg="yellow200">
        1
      </Box>
      <Box p="md" h="40px" bg="tomato">
        2
      </Box>
      <Box p="md" h="40px" bg="pink100">
        3
      </Box>
    </Stack>
  ),
};

export const horizontalStack = {
  argTypes: {
    direction: {
      name: "direction",
      type: { name: "string", required: false },
      defaultValue: "row",
      description: "Shorthand for flexDirection style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "row" },
      },
      control: {
        type: "select",
        options: ["row", "row-reverse"],
      },
    },
    spacing: spacingSelect,
    align: alignSelect,
  },
  render: (args: HStackProps) => (
    <HStack h="50vh" {...args}>
      <Box p="md" bg="yellow200">
        1
      </Box>
      <Box p="md" bg="tomato">
        2
      </Box>
      <Box p="md" bg="pink100">
        3
      </Box>
    </HStack>
  ),
};

export const verticalStack = {
  argTypes: {
    direction: {
      name: "direction",
      type: { name: "string", required: false },
      defaultValue: "column",
      description: "Shorthand for flexDirection style prop",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "row" },
      },
      control: {
        type: "select",
        options: ["column", "column-reverse"],
      },
    },
    spacing: spacingSelect,
    align: alignSelect,
  },
  render: (args: VStackProps) => (
    <VStack {...args}>
      <Box p="md" h="40px" bg="yellow200">
        1
      </Box>
      <Box p="md" h="40px" bg="tomato">
        2
      </Box>
      <Box p="md" h="40px" bg="pink100">
        3
      </Box>
    </VStack>
  ),
};
```

- Now run `npm run storybook` check the 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/index.ts` file and paste the following -

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

- Now `npm run build`.

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

```tsx
import * as React from "react";
import { VStack, Box } from "chakra-ui-clone";

export function App() {
  return (
    <VStack m="1rem" align="stretch">
      <Box p="md" h="40px" bg="yellow200">
        1
      </Box>
      <Box p="md" h="40px" bg="tomato">
        2
      </Box>
      <Box p="md" h="40px" bg="pink100">
        3
      </Box>
    </VStack>
  );
}
```

### Summary

There you go guys in this tutorial we created `Stack` components just like chakra ui and stories for them. You can find the code for this tutorial under the **atom-layout-stack** branch [here](https://github.com/yaldram/chakra-ui-clone). In the next tutorial we will create some container components. Until next time PEACE.