DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

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

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. All the code for this series is available on GitHub.

Step One: Button styles & theming

We want to achieve the following for the Button component -

<Button isRounded variant='bordered' isAuto colorScheme='success'> 
  Success
</Button>
Enter fullscreen mode Exit fullscreen mode

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 -

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

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 -

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

The above code is pretty straightforward.

Step Three: Button story

Under atoms/button create a new file button.stories.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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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. Until next time PEACE.

Top comments (0)