DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Building a design system using Solidjs, Typescript, SCSS, CSS Variables and Vite - Box Component

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

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

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

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

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

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

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

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

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

Finally, under atoms/index.ts paste -

export * from "./layouts";
Enter fullscreen mode Exit fullscreen mode

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)