Building a design system using Solidjs, Typescript, SCSS, CSS Variables and Vite - Button Component
Tags: solidjs, typescript, css, vite
Published: 2023-04-15 15:23:25

### Introduction
This is part five of our series on building a design system using Solidjs. In the previous tutorial we created our first theme able `Badge` component. In this tutorial we will create our theme able component Button component with light and dark modes. 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 series is available on [GitHub](https://github.com/yaldram/solid-vite-lib).

### Step One: Button styles & theming
We want to achieve the following for the `Button` component - 
```tsx
<Button isRounded variant='bordered' isAuto colorScheme='success'> 
  Success
</Button>
```
We have the `colorScheme` prop, taking values - success, error, warning, etc. We also pass the variant prop we can pass it values `solid, ghost, flat, etc.`. Basically, we have the `colorScheme` & `variant props` combination. We also have 3 size variants - sm, md, lg.

Under `components/atoms` create a new folder `forms`, under `forms` create an `index.ts` file and a new folder `button`. Finally under `components/atoms/forms/button` create a new file `button.scss` - 
```scss
.button {
  appearance: none;
  box-sizing: border-box;
  font-weight: $font-size-sm;

  display: flex;
  align-items: center;
  justify-content: center;

  line-height: 1.25;
  user-select: none;
  text-align: center;
  white-space: nowrap;
  border: none;
  cursor: pointer;
  padding: 0;

  transition: background 0.25s ease 0s, 
  color 0.25s ease 0s, 
  border-color 0.25s ease 0s, 
  box-shadow 0.25s ease 0s, 
  transform 0.25s ease 0s, 
  opacity 0.25s ease 0s;

  border-width: $border-weight-normal;

  &:active {
    transform: scale(0.97);
  }

  &.xs {
    border-radius: $radii-xs;
    height: $space-10;
    padding-left: $space-3;
    padding-right: $space-3;
    line-height: $space-10;
    min-width: $space-20; 
    font-size: $font-size-xs;
  }

  &.sm {
    border-radius: $radii-sm;
    height: $space-12;
    padding-left: $space-5;
    padding-right: $space-5;
    line-height: $space-14;
    min-width: $space-36; 
    font-size: $font-size-sm;
  }

  &.md {
    border-radius: $radii-md;
    height: $space-14;
    padding-left: $space-7;
    padding-right: $space-7;
    line-height: $space-14;
    min-width: $space-48; 
    font-size: $font-size-sm;
  }

  &.lg {
    border-radius: $radii-base;
    height: $space-16;
    padding-left: $space-9;
    padding-right: $space-9;
    line-height: $space-15;
    min-width: $space-60; 
    font-size: $font-size-md;
  }

  &.xl {
    border-radius: $radii-xl;
    height: $space-18;
    padding-left: $space-10;
    padding-right: $space-10;
    line-height: $space-17;
    min-width: $space-72; 
    font-size: $font-size-lg;
  }

  &.is-rounded {
    border-radius: $radii-pill;
  }

  &.is-auto {
    width: auto;
    min-width: min-content;
  }

  &.solid {
    @each $scheme in $color-schemes {
      $base: --color-#{$scheme}; // --color-primary
      $contrast: #{$base}-solid-contrast;  // --color-primary-solid-contrast
      &.#{$scheme} {
        background: var($base);
        color: var($contrast);
      }
    }
  }

  &.is-shadow {
    @each $scheme in $color-schemes {
      &.#{$scheme} {
        box-shadow: 0 4px 14px 0 var(--color-#{$scheme}-shadow);
      }
    }
  }

  &.bordered {
    background-color: transparent;
    border-style: solid;

    @each $scheme in $color-schemes {
      &.#{$scheme} {
        color: var(--color-#{$scheme});
        border-color: var(--color-#{$scheme});
      }
    }
  }

  &.ghost {
    background: transparent;
    border-style: solid;

    @each $scheme in $color-schemes {
      $base: --color-#{$scheme}; // --color-primary
      $contrast: #{$base}-solid-contrast;  // --color-primary-solid-contrast
      
      &.#{$scheme} {
        color: var($base);
        border-color: var($base);
        &:hover {
          color: var($contrast);
          background-color: var($base);
        } 
      }
    }
  }

  &.light {
    background-color: transparent;

    @each $scheme in $color-schemes {
      $base: --color-#{$scheme}; // --color-primary
      $active: #{$base}-light-active;  // --color-primary-light-active
      
      &.#{$scheme} {
        color: var($base);
        &:active {
          background-color: var($active);
        } 
      }
    }
  }

  &.flat {
    &.neutral {
      background-color: var(--color-neutral-light);
      color: var(--color-neutral-light-contrast);
      &:hover {
       background-color: var(--color-neutral-light-hover);
      }  
      &:active {
        background-color: var(--color-neutral-light-active);
      } 
    }

    &.primary {
      background-color: var(--color-primary-light);
      color: var(--color-primary-light-contrast);
      &:hover {
       background-color: var(--color-primary-light-hover);
      }  
      &:active {
        background-color: var(--color-primary-light-active);
      } 
    }

    &.secondary {
      background-color: var(--color-secondary-light);
      color: var(--color-secondary-light-contrast);
      &:hover {
        background-color: var(--color-secondary-light-hover);
      } 
      &:active {
        background-color: var(--color-secondary-light-active);
      } 
    }

    &.warning {
      background-color: var(--color-warning-light);
      color: var(--color-warning-light-contrast);
      &:hover {
        background-color: var(--color-warning-light-hover);
      } 
      &:active {
        background-color: var(--color-warning-light-active);
      } 
    }

    &.success {
      background-color: var(--color-success-light);
      color: var(--color-success-light-contrast);
      &:hover {
        background-color: var(--color-success-light-hover);
      } 
      &:active {
        background-color: var(--color-success-light-active);
      } 
    }

    &.error {
      background-color: var(--color-error-light);
      color: var(--color-error-light-contrast);
      &:hover {
        background-color: var(--color-error-light-hover);
      } 
      &:active {
        background-color: var(--color-error-light-active);
      } 
    }
  }
}
```
For the css classes we have to - 
1. First we created the base `.button` class.
2. Then we created the size variants classes `sm, md, lg` scoping all of them under the button class.
3. We then create colorScheme classes using the rightful colors for each colorScheme. We then combine colorScheme with variants like `flat`, `bordered` - `.flat .warning, .bordered .success`, we used scss loops to create all these combinations. 
       
### Step Two: Button component
Under `forms/button` create a new file `index.tsx` -
```tsx
import { Component, ComponentProps, splitProps } from 'solid-js'
import { cva, VariantProps } from 'class-variance-authority'

import { ColorScheme } from '../../../../cva-utils'

import './button.scss'

const button = cva(['button'], {
  variants: {
    size: {
      xs: 'xs',
      sm: 'sm',
      md: 'md',
      lg: 'lg',
      xl: 'xl'
    },
    isRounded: {
      true: 'is-rounded'
    },
    isAuto: {
      true: 'is-auto'
    },
    isShadow: {
      true: 'is-shadow'
    },
    variant: {
      solid: 'solid',
      ghost: 'ghost',
      flat: 'flat',
      light: 'light',
      bordered: 'bordered'
    }
  },
  defaultVariants: {
    variant: 'solid',
    size: 'md'
  }
})

export type ButtonProps = VariantProps<typeof button> &
  ComponentProps<'button'> & {
    colorScheme?: ColorScheme
  }

export const Button: Component<ButtonProps> = (props) => {
  const [variants, colorScheme, children, delegated] = splitProps(
    props,
    ['size', 'isRounded', 'isAuto', 'isShadow', 'variant'],
    ['colorScheme'],
    ['children']
  )

  return (
    <button
      class={button({
        isAuto: variants.isAuto,
        size: variants.size,
        isRounded: variants.isRounded,
        isShadow: variants.isShadow,
        variant: variants.variant,
        className: colorScheme.colorScheme || 'primary'
      })}
      {...delegated}
    >
      {children.children}
    </button>
  )
}   
```
The above code is pretty straightforward.

### Step Three: Button story
Under `atoms/button` create a new file `button.stories.tsx` - 
```tsx
/** @jsxImportSource solid-js */

import { StoryObj } from 'storybook-solidjs'

import { colorSchemes } from '../../../../cva-utils'
import { Flex } from '../../layouts'
import { Button, ButtonProps } from '.'

export default {
  title: 'Atoms/Forms/Button'
}

export const Playground: StoryObj<ButtonProps> = {
  parameters: {
    theme: 'split'
  },
  args: {
    colorScheme: 'primary',
    variant: 'solid',
    size: 'md',
    isRounded: false,
    isAuto: false,
    isShadow: false
  },
  argTypes: {
    colorScheme: {
      name: 'colorScheme',
      type: { name: 'string', required: false },
      options: colorSchemes,
      description: 'The Color Scheme for the button',
      table: {
        type: { summary: 'string' },
        defaultValue: { summary: 'green' }
      },
      control: {
        type: 'select'
      }
    },
    size: {
      name: 'size (s)',
      type: { name: 'string', required: false },
      options: ['xs', 'sm', 'md', 'lg', 'xl'],
      description: 'Button height width and horizontal padding',
      table: {
        type: { summary: 'string' },
        defaultValue: { summary: 'md' }
      },
      control: {
        type: 'select'
      }
    },
    variant: {
      name: 'variant',
      type: { name: 'string', required: false },
      options: ['solid', 'bordered', 'ghost', 'flat', 'light'],
      description: 'Button Variant',
      table: {
        type: { summary: 'string' },
        defaultValue: { summary: 'md' }
      },
      control: {
        type: 'select'
      }
    },
    isRounded: {
      name: 'isRounded',
      type: { name: 'boolean', required: false },
      description: 'Is Rounded prop',
      table: {
        type: { summary: 'boolean' },
        defaultValue: { summary: 'false' }
      },
      control: {
        type: 'boolean'
      }
    },
    isAuto: {
      name: 'isAuto',
      type: { name: 'boolean', required: false },
      description: 'Is Auto prop',
      table: {
        type: { summary: 'boolean' },
        defaultValue: { summary: 'false' }
      },
      control: {
        type: 'boolean'
      }
    },
    isShadow: {
      name: 'isShadow',
      type: { name: 'boolean', required: false },
      description: 'Is shadow prop',
      table: {
        type: { summary: 'boolean' },
        defaultValue: { summary: 'false' }
      },
      control: {
        type: 'boolean'
      }
    }
  },
  render: (args) => <Button {...args}>Button Component</Button>
}

export const Default = {
  render: () => (
    <Flex direction='col' gap='xl'>
      <Flex gap='md' wrap='wrap'>
        <Button>Primary</Button>
        <Button colorScheme='secondary'>Secondary</Button>
        <Button colorScheme='success'>Success</Button>
        <Button colorScheme='warning'>Warning</Button>
        <Button colorScheme='error'>Error</Button>
      </Flex>
      <Flex gap='md'>
        <Button isAuto>Primary</Button>
        <Button isAuto colorScheme='secondary'>
          Secondary
        </Button>
        <Button isAuto colorScheme='success'>
          Success
        </Button>
        <Button isAuto colorScheme='warning'>
          Warning
        </Button>
        <Button isAuto colorScheme='error'>
          Error
        </Button>
      </Flex>
      <Flex gap='md'>
        <Button isShadow isAuto>
          Primary
        </Button>
        <Button isShadow isAuto colorScheme='secondary'>
          Secondary
        </Button>
        <Button isShadow isAuto colorScheme='success'>
          Success
        </Button>
        <Button isShadow isAuto colorScheme='warning'>
          Warning
        </Button>
        <Button isShadow isAuto colorScheme='error'>
          Error
        </Button>
      </Flex>
      <Flex gap='md'>
        <Button variant='bordered' isAuto>
          Primary
        </Button>
        <Button variant='bordered' isAuto colorScheme='secondary'>
          Secondary
        </Button>
        <Button variant='bordered' isAuto colorScheme='success'>
          Success
        </Button>
        <Button variant='bordered' isAuto colorScheme='warning'>
          Warning
        </Button>
        <Button variant='bordered' isAuto colorScheme='error'>
          Error
        </Button>
      </Flex>
      <Flex gap='md'>
        <Button isRounded isAuto>
          Primary
        </Button>
        <Button isRounded isAuto colorScheme='secondary'>
          Secondary
        </Button>
        <Button isRounded isAuto colorScheme='success'>
          Success
        </Button>
        <Button isRounded isAuto colorScheme='warning'>
          Warning
        </Button>
        <Button isRounded isAuto colorScheme='error'>
          Error
        </Button>
      </Flex>
      <Flex gap='md'>
        <Button isRounded variant='bordered' isAuto>
          Primary
        </Button>
        <Button isRounded variant='bordered' isAuto colorScheme='secondary'>
          Secondary
        </Button>
        <Button isRounded variant='bordered' isAuto colorScheme='success'>
          Success
        </Button>
        <Button isRounded variant='bordered' isAuto colorScheme='warning'>
          Warning
        </Button>
        <Button isRounded variant='bordered' isAuto colorScheme='error'>
          Error
        </Button>
      </Flex>
      <Flex gap='md'>
        <Button variant='ghost' isAuto>
          Primary
        </Button>
        <Button variant='ghost' isAuto colorScheme='secondary'>
          Secondary
        </Button>
        <Button variant='ghost' isAuto colorScheme='success'>
          Success
        </Button>
        <Button variant='ghost' isAuto colorScheme='warning'>
          Warning
        </Button>
        <Button variant='ghost' isAuto colorScheme='error'>
          Error
        </Button>
      </Flex>
      <Flex gap='md'>
        <Button variant='flat' isAuto>
          Primary
        </Button>
        <Button variant='flat' isAuto colorScheme='secondary'>
          Secondary
        </Button>
        <Button variant='flat' isAuto colorScheme='success'>
          Success
        </Button>
        <Button variant='flat' isAuto colorScheme='warning'>
          Warning
        </Button>
        <Button variant='flat' isAuto colorScheme='error'>
          Error
        </Button>
      </Flex>
      <Flex gap='md'>
        <Button variant='light' isAuto>
          Primary
        </Button>
        <Button variant='light' isAuto colorScheme='secondary'>
          Secondary
        </Button>
        <Button variant='light' isAuto colorScheme='success'>
          Success
        </Button>
        <Button variant='light' isAuto colorScheme='warning'>
          Warning
        </Button>
        <Button variant='light' isAuto colorScheme='error'>
          Error
        </Button>
      </Flex>
    </Flex>
  )
}
```
From the terminal run `yarn storybook` and check the button stories.

### Conclusion
In this tutorial we created theme able `Button` component. All the code for this tutorial can be found [here](https://github.com/yaldram/solid-vite-lib). Until next time PEACE.