Building a design system using Solidjs, Typescript, SCSS, CSS Variables and Vite - Box Component
Tags: solidjs, typescript, css, vite
Published: 2023-04-15 12:22:39

### 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](http://solid-vite-lib.s3-website.ap-south-1.amazonaws.com/?path=/story/atoms-forms-button--playground). 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` - 
```scss
.box {
  box-sizing: border-box;
}
```

Now under `atoms/layout/box/index.tsx` paste the following - 
```tsx
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 - 
```tsx
<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 - 
```tsx
/** @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 -
```scss
/* 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 - 
```tsx
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 - 
```tsx
<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` - 
```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 - 
```ts
export * from "./box";
export * from "./flex";
```

Finally, under `atoms/index.ts` paste - 
```ts
export * from "./layouts";
```


### Conclusion
In this tutorial we created the `Box` & `Flex` components. All the code for this tutorial can be found [here](https://github.com/yaldram/solid-vite-lib). In the next tutorial we will create our first theme able component `Badge` with both light and dark modes. Until next time PEACE.