Introduction
This is part three of our series on building a design system using Solidjs. In the previous tutorial we created the theme / design tokens and the atomic classes. In this tutorial we will create our layout components Box
& Flex
. I would encourage you to play around with the deployed storybook. All the code for this tutorial available on [GitHub(https://github.com/yaldram/solid-vite-lib).
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 { Component, ComponentProps, splitProps } from 'solid-js'
import { cva, cx } from 'class-variance-authority'
import {
colors,
ColorVariants,
bgColors,
BgColorVariants,
padding,
PaddingVariants,
margin,
MarginVariants
} from '../../../../cva-utils'
import './box.scss'
const box = cva(['box'])
export type BoxProps = ColorVariants &
BgColorVariants &
PaddingVariants &
MarginVariants &
ComponentProps<'div'>
export const Box: Component<BoxProps> = (props) => {
const [colorClasses, paddingClasses, marginClasses, className, delegated] =
splitProps(
props,
['color', 'bg'],
['p', 'px', 'py', 'pt', 'pr', 'pb', 'pl'],
['m', 'mx', 'my', 'mt', 'mr', 'mb', 'ml'],
['class']
)
return (
<div
class={cx(
colors({ color: colorClasses.color }),
bgColors({ bg: colorClasses.bg }),
padding({ ...paddingClasses }),
margin({ ...marginClasses }),
box({ className: className.class })
)}
{...delegated}
/>
)
}
Take a note, we are using splitProps
because in Solidjs
we cannot de-structure props directly.
We imported all the cva variants that will become our utility props, merged them into one class using the cx function. Here is how we will use the Box
component, with all our theme tokens turned into utility props -
<Box bg="red800" color="white" p="md" m="sm">
This is a Box component.
</Box>
Now create a box.stories.tsx
and paste the following -
/** @jsxImportSource solid-js */
import { spacingControls } from '../../../../cva-utils'
import { Box } from '.'
export default {
title: 'Atoms/Layout/Box'
}
const { spacingOptions, spacingLabels } = spacingControls()
export const Playground = {
args: {
p: 'sm',
m: 'sm',
bg: 'red500'
},
argTypes: {
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%' }} bg='red800' color='white' p='xl'>
Box Component
</Box>
)
Now from the terminal run yarn storybook
and check the output.
Step Two: Create the Flex component
Things will get clearer, 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 { Component, splitProps } from 'solid-js'
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'
},
wrap: {
wrap: 'flex-wrap',
'wrap-reverse': 'flex-wrap-reverse',
nowrap: 'flex-nowrap'
}
},
defaultVariants: {
direction: 'row'
}
})
export type FlexProps = VariantProps<typeof flex> & FlexGapVariants & BoxProps
export const Flex: Component<FlexProps> = (props) => {
const [variants, className, delegated] = splitProps(
props,
['direction', 'justify', 'align', 'gap', 'wrap'],
['class']
)
return (
<Box
class={cx(
flexGap({ gap: variants.gap }),
flex({
direction: variants.direction,
justify: variants.justify,
align: variants.align,
wrap: variants.wrap,
className: className.class
})
)}
{...delegated}
/>
)
}
export interface SpacerProps extends BoxProps {}
export const Spacer: Component<SpacerProps> = (props) => {
return <Box class='spacer' {...props} />
}
To the cva function we first passed the main flex
class and created variants for our various utility props. We then merge the classes 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
-
/** @jsxImportSource solid-js */
import { Component } from 'solid-js'
import { spacingControls } from '../../../../cva-utils'
import { Flex, FlexProps, Spacer } from '.'
const { spacingOptions, spacingLabels } = spacingControls()
export default {
title: 'Atoms/Layout/Flex'
}
const Container: Component<FlexProps> = (props) => {
return (
<Flex
style={{ 'min-height': '100px', 'min-width': '100px' }}
justify='center'
align='center'
{...props}
/>
)
}
export const Playground = {
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='green600'>Box 1</Container>
<Container bg='blue600'>Box 2</Container>
<Container bg='purple600'>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='gray700'
p='xs'
{...args}
>
<Container p='md' bg='red600'>
Box 1
</Container>
<Spacer />
<Container p='md' bg='green600'>
Box 2
</Container>
</Flex>
)
}
export const Stack = {
args: {
direction: 'row',
gap: 'md',
align: 'start'
},
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%', 'min-height': '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='purple600'>
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)