DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on • Updated on

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

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.
  • 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.

Prerequisite

Please check the previous post where we have completed the Flex Component. Also please check the Chakra Stack Component code here.

In this tutorial we will be using variants from styled-system, therefore I would advise you to check check my introductory post.

Check the variant function docs.

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 -
git checkout -b atom-layout-stack
Enter fullscreen mode Exit fullscreen mode
  • 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 -
import * as React from "react";

export function getValidChildren(children: React.ReactNode) {
  return React.Children.toArray(children).filter((child) =>
    React.isValidElement(child)
  ) as React.ReactElement[];
}
Enter fullscreen mode Exit fullscreen mode
  • Under utils/index.ts paste the following -
export * from "./dom";
Enter fullscreen mode Exit fullscreen mode
  • Under src/components/atoms/layout/stack/index.tsx lets import the necessary stuff -
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";
Enter fullscreen mode Exit fullscreen mode
  • 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 -
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),
    })}
`;
Enter fullscreen mode Exit fullscreen mode
  • 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.

<Stack direction={["row", "column"]}></Stack>
Enter fullscreen mode Exit fullscreen mode
  • Now let us create the Stack component -
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>
    );
  }
);
Enter fullscreen mode Exit fullscreen mode
  • Note a few things we have Partial, 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 -

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

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 -

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

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.
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>
  ),
};
Enter fullscreen mode Exit fullscreen mode
  • 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 -
export * from "./box";
export * from "./flex";
export * from "./stack";
Enter fullscreen mode Exit fullscreen mode
  • 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.

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

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. In the next tutorial we will create some container components. Until next time PEACE.

Discussion (0)