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 theStack
component and further extend theStack
component to create aHStack
andVStack
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
Under the
components/atoms/layout
folder create a new folder called stack. Under stack folder create 2 filesindex.tsx
andstack.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 calledutils
. 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[];
}
- Under utils/index.ts paste the following -
export * from "./dom";
- 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";
- The Chakra stack component takes in 2 important props namely
direction
for the stack andspacing
for the elements. Therefore what I did was, I used thevariant
function for thedirection
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),
})}
`;
First I created the type
StackVariants
, for the direction prop. Then I created theBaseStackProps
type that extend theFlexProps
&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>
- 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>
);
}
);
- 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>
);
}
);
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>
);
}
);
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>
),
};
- 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";
Now
npm run build
.Under the folder
example/src/App.tsx
we can test our Stack components. Copy paste the following code and runnpm run start
from theexample
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>
);
}
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.
Top comments (0)