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. All the code for this series is available on GitHub.
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
-
.box {
box-sizing: border-box;
}
Now under atoms/layout/box/index.tsx
paste the following -
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 -
<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 -
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 -
/* 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 -
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 -
<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
-
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 -
export * from "./box";
export * from "./flex";
Finally, under atoms/index.ts
paste -
export * from "./layouts";
Conclusion
In this tutorial we created the Box
& Flex
components. All the code for this tutorial can be found here. In the next tutorial we will create our first theme able component Badge
with both light and dark modes. Until next time PEACE.
Top comments (0)