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

### Introduction
This is part four of our series on building a design system using Solidjs. In the previous tutorial we created our `Box` & `Flex` components. In this tutorial we will create our first theme able component Badge 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-badge--playground). All the code for this series is available on [GitHub](https://github.com/yaldram/solid-vite-lib).

### Step One: Badge styles & theming
We want to achieve the following for the `Badge` component - 
```tsx
<Badge colorScheme="success" isFlat isSquared size="md">
  Sample Badge
</Badge>
```
We have the `colorScheme` prop, taking values - success, error, warning, etc. We can pass 3 variant props to the component - isFlat, isSquared, bordered. Basically, we have the `colorScheme` & `variant props` combination. We also have 3 size variants - sm, md, lg.

In the previous iteration of our [design-system](https://dev.to/yaldram/building-a-design-system-with-dark-mode-using-react-typescript-scss-cva-and-vite-badge-component-34fj) for handling the light and dark modes we will created 2 different classes using the data attribute selector `[data-theme="dark"]`. In this tutorial series we will use css variables for theming, all the CSS variables are under the `scss/themes` folder.

The consumer of our library will add the `root` class and the `light-theme` / `dark-theme` class to the root of the project, we won't be creating any `ThemeProvider` because we don't need it.

Under `components/atoms` create a new folder `badge` and under `badge` folder create a new file `badge.scss` - 

```scss
.badge {
  --badge-font-size: none;
  --badge-shadow-color: none;

  box-sizing: border-box;
  line-height: 1;
  white-space: nowrap;
  
  font-weight: 700;
  font-size: var(--badge-font-size);

  border-radius: $radii-pill;

  &.xs {
    padding: $space-2 $space-2;
    --badge-font-size: 0.65rem;
  }

  &.sm {
    padding: $space-2 $space-3;
    --badge-font-size: 0.73rem;
  }

  &.md {
    padding: $space-3 $space-4;
    --badge-font-size: #{$font-size-xs};
  }

  &.lg {
    padding: $space-4 $space-5;
    --badge-font-size: #{$font-size-base};
  }

  &.xl {
    padding: $space-5 $space-6;
    --badge-font-size: #{$font-size-xl};
  }

  &.neutral {
    background-color: var(--color-neutral);
    color: var(--color-neutral-solid-contrast);
    --badge-shadow-color: var(--color-neutral-shadow);
  }

  &.primary {
    background-color: var(--color-primary);
    color: var(--color-primary-solid-contrast);
    --badge-shadow-color: var(--color-primary-shadow);
  }

  &.secondary {
    background-color: var(--color-secondary);
    color: var(--color-secondary-solid-contrast);
    --badge-shadow-color: var(--color-secondary-shadow);
  }
  
  &.success {
    background-color: var(--color-success);
    color: var(--color-success-solid-contrast);
    --badge-shadow-color: var(--color-success-shadow);
  }

  &.warning {
    background-color: var(--color-warning);
    color: var(--color-warning-solid-contrast);
    --badge-shadow-color: var(--color-warning-shadow);
  }

  &.error {
    background-color: var(--color-error);
    color: var(--color-error-solid-contrast);
    --badge-shadow-color: var(--color-error-shadow);
  }

  &.enable-shadow {
    box-shadow: 0 2px 10px 0 var(--badge-shadow-color);
  }

  &.is-squared {
    border-radius: calc(var(--badge-font-size) * 0.45);
  }

  &.is-flat {
    @each $scheme in $color-schemes {
      $bg: --color-#{$scheme}-light; // --color-primary-light
      $color: #{$bg}-contrast; // --color-primary-light-contrast
      
      &.#{$scheme} {
        background-color: #{var($bg)};
        color: #{var($color)};
      }
     }
  }

  &.is-bordered {
   background-color: var(--background);
   border-width: 2px;
   border-style: solid;

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

For the css classes we have to - 
1. First we created the base `.badge` class.
2. Then we created the size variants classes `sm, md, lg` scoping all of them under the badge class.
3. We then create colorScheme classes using the rightful colors for each colorScheme. We then combine colorScheme with variants like `is-flat`, `is-bordered` - `.is-flat .warning, .is-bordered .success`, we used scss map function to create all these combinations. 
4. Take a note we are using the local css variables like the `--badge-shadow-color` setting appropriate values for each colorScheme. You need to be smart about using local CSS variables, Scss functions for such use cases.

I would encourage you to check out my [previous tutorial series](https://dev.to/yaldram/building-a-design-system-with-dark-mode-using-react-typescript-scss-cva-and-vite-badge-component-34fj) to check the amount of code we have reduced, because we are using CSS variables for our theming.

Notice one thing, we have not imported $color-schemes from our variables folder, instead of importing our variables in every .scss file we will add the following configuration to vite's `defineConfig` function - 
```ts
css: {
    preprocessorOptions: {
      scss: {
        additionalData: `
          @use "./src/scss/variables/_borders.scss" as *;
          @use "./src/scss/variables/_fonts.scss" as *;
          @use "./src/scss/variables/_radii.scss" as *;
          @use "./src/scss/variables/_spacings.scss" as *;
          @use "./src/scss/variables/_colors.scss" as *;
        `
      }
    }
  },
```
It will add these variables to all `scss` files that are imported in `.tsx` files, like we imported our `badge.scss` file in the `badge/index.tsx` file.

### Step Two: Badge component
Under `atoms/badge` create a new file `index.tsx` -
```tsx
import { cva, VariantProps } from 'class-variance-authority'
import { Component, ComponentProps, mergeProps, splitProps } from 'solid-js'

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

import './badge.scss'

const badge = cva(['badge'], {
  variants: {
    size: {
      xs: 'xs',
      sm: 'sm',
      md: 'md',
      lg: 'lg',
      xl: 'xl'
    },
    enableShadow: {
      true: 'enable-shadow'
    },
    bordered: {
      true: 'is-bordered'
    },
    isFlat: {
      true: 'is-flat'
    },
    isSquared: {
      true: 'is-squared'
    }
  },
  defaultVariants: {
    size: 'md'
  }
})

export type BadgeProps = VariantProps<typeof badge> &
  ComponentProps<'span'> & {
    colorScheme?: ColorScheme
  }

export const Badge: Component<BadgeProps> = (props) => {
  const mergedProps = mergeProps({ colorScheme: 'neutral' }, props)

  const [variants, colorScheme, delegated] = splitProps(
    mergedProps,
    ['size', 'enableShadow', 'bordered', 'isFlat', 'isSquared'],
    ['colorScheme']
  )

  return (
    <span
      class={badge({
        size: variants.size,
        enableShadow: variants.enableShadow,
        bordered: variants.bordered,
        isFlat: variants.isFlat,
        isSquared: variants.isSquared,
        className: colorScheme.colorScheme
      })}
      {...delegated}
    />
  )
}    
```
The above code is pretty straightforward.

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

import { colorSchemes } from '../../../cva-utils'
import { Flex } from '../layouts'
import { Badge, BadgeProps } from '.'
import { StoryObj } from 'storybook-solidjs'

export default {
  title: 'Atoms/Badge'
}

export const Default: StoryObj<BadgeProps> = {
  args: {
    size: 'md'
  },
  argTypes: {
    size: {
      name: 'size (s)',
      type: { name: 'string', required: false },
      options: ['xs', 'sm', 'md', 'lg', 'xl'],
      description: 'Tag height width and horizontal padding',
      table: {
        type: { summary: 'string' },
        defaultValue: { summary: 'md' }
      },
      control: {
        type: 'select'
      }
    }
  },
  render: (args) => (
    <Flex direction='col' gap='lg'>
      <Flex gap='xl'>
        <Badge {...args}>Neutral</Badge>
        <Badge {...args} colorScheme='primary'>
          Primary
        </Badge>
        <Badge {...args} colorScheme='secondary'>
          Secondary
        </Badge>
        <Badge {...args} colorScheme='success'>
          Success
        </Badge>
        <Badge {...args} colorScheme='warning'>
          Warning
        </Badge>
        <Badge {...args} colorScheme='error'>
          Error
        </Badge>
      </Flex>
      <Flex gap='xl'>
        <Badge {...args} enableShadow>
          Neutral
        </Badge>
        <Badge {...args} enableShadow colorScheme='primary'>
          Primary
        </Badge>
        <Badge {...args} enableShadow colorScheme='secondary'>
          Secondary
        </Badge>
        <Badge {...args} enableShadow colorScheme='success'>
          Success
        </Badge>
        <Badge {...args} enableShadow colorScheme='warning'>
          Warning
        </Badge>
        <Badge {...args} enableShadow colorScheme='error'>
          Error
        </Badge>
      </Flex>
      <Flex gap='xl'>
        <Badge {...args} bordered>
          Neutral
        </Badge>
        <Badge {...args} bordered colorScheme='primary'>
          Primary
        </Badge>
        <Badge {...args} bordered colorScheme='secondary'>
          Secondary
        </Badge>
        <Badge {...args} bordered colorScheme='success'>
          Success
        </Badge>
        <Badge {...args} bordered colorScheme='warning'>
          Warning
        </Badge>
        <Badge {...args} bordered colorScheme='error'>
          Error
        </Badge>
      </Flex>
      <Flex gap='xl'>
        <Badge {...args} isFlat>
          Neutral
        </Badge>
        <Badge {...args} isFlat colorScheme='primary'>
          Primary
        </Badge>
        <Badge {...args} isFlat colorScheme='secondary'>
          Secondary
        </Badge>
        <Badge {...args} isFlat colorScheme='success'>
          Success
        </Badge>
        <Badge {...args} isFlat colorScheme='warning'>
          Warning
        </Badge>
        <Badge {...args} isFlat colorScheme='error'>
          Error
        </Badge>
      </Flex>
      <Flex gap='xl'>
        <Badge {...args} isSquared>
          Neutral
        </Badge>
        <Badge {...args} isSquared colorScheme='primary'>
          Primary
        </Badge>
        <Badge {...args} isSquared colorScheme='secondary'>
          Secondary
        </Badge>
        <Badge {...args} isSquared colorScheme='success'>
          Success
        </Badge>
        <Badge {...args} isSquared colorScheme='warning'>
          Warning
        </Badge>
        <Badge {...args} isSquared colorScheme='error'>
          Error
        </Badge>
      </Flex>
      <Flex align='start' gap='xl'>
        <Badge {...args} isSquared>
          Neutral
        </Badge>
        <Badge {...args} isSquared bordered colorScheme='primary'>
          Primary
        </Badge>
        <Badge {...args} isSquared isFlat colorScheme='secondary'>
          Secondary
        </Badge>
        <Badge {...args} isSquared colorScheme='success'>
          Success
        </Badge>
        <Badge {...args} isSquared bordered colorScheme='warning'>
          Warning
        </Badge>
        <Badge {...args} isSquared isFlat colorScheme='error'>
          Error
        </Badge>
      </Flex>
    </Flex>
  )
}

export const Playground: StoryObj<BadgeProps> = {
  parameters: {
    theme: 'split'
  },
  args: {
    colorScheme: 'success',
    size: 'md',
    bordered: false,
    isFlat: false,
    isSquared: false,
    enableShadow: 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: 'Tag height width and horizontal padding',
      table: {
        type: { summary: 'string' },
        defaultValue: { summary: 'md' }
      },
      control: {
        type: 'select'
      }
    },
    bordered: {
      name: 'bordered',
      type: { name: 'boolean', required: false },
      description: 'Is Bordered',
      table: {
        type: { summary: 'boolean' },
        defaultValue: { summary: 'false' }
      },
      control: {
        type: 'boolean'
      }
    },
    isFlat: {
      name: 'isFlat',
      type: { name: 'boolean', required: false },
      description: 'Is Flat',
      table: {
        type: { summary: 'boolean' },
        defaultValue: { summary: 'false' }
      },
      control: {
        type: 'boolean'
      }
    },
    isSquared: {
      name: 'isSquared',
      type: { name: 'boolean', required: false },
      description: 'Is Bordered',
      table: {
        type: { summary: 'boolean' },
        defaultValue: { summary: 'false' }
      },
      control: {
        type: 'boolean'
      }
    },
    enableShadow: {
      name: 'enableShadow',
      type: { name: 'boolean', required: false },
      description: 'enableShadow',
      table: {
        type: { summary: 'boolean' },
        defaultValue: { summary: 'false' }
      },
      control: {
        type: 'boolean'
      }
    }
  },
  render: (args) => <Badge {...args}>Sample Badge</Badge>
}
```
From the terminal run `yarn storybook` and check the badge stories.

### Step Four: Create a theme addon in Storybook
`Badge` component works both for light and dark mode, but we don't have a way in storybook to switch themes. Well we have to build one. I would recommend, you read this [awesome article](https://storybook.js.org/blog/how-to-add-a-theme-switcher-to-storybook/) on how to create a theme switcher in storybook.

Under `.storybook/preview.tsx` file paste the following - 
```tsx
/** @jsxImportSource solid-js */
import { Component } from 'solid-js'

import { Flex, FlexProps } from '../src/components/atoms'

import '../src/scss/main.scss'

const Container: Component<FlexProps> = (props) => {
  return (
    <Flex
      align='start'
      p='md'
      style='min-height: 100vh; flex-basis: 50%;'
      {...props}
    />
  )
}

const preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/
      }
    }
  }
}

export const decorators = [
  (StoryFun, context) => {
    const theme = context.parameters.theme || context.globals.theme

    if (theme === 'split') {
      return (
        <Flex>
          <Container class='root light-theme' bg='white'>
            <StoryFun />
          </Container>
          <Container class='root dark-theme' bg='black'>
            <StoryFun />
          </Container>
        </Flex>
      )
    }

    return (
      <Container
        class={theme === 'dark' ? 'root dark-theme' : 'root light-theme'}
        bg={theme === 'dark' ? 'black' : 'white'}
      >
        <StoryFun />
      </Container>
    )
  }
]

export const globalTypes = {
  theme: {
    name: 'Change Theme',
    description: 'Global theme for components',
    defaultValue: 'light',
    toolbar: {
      // The icon for the toolbar item
      icon: 'circlehollow',
      // Array of options
      items: [
        { value: 'light', icon: 'circlehollow', title: 'light-view' },
        { value: 'dark', icon: 'circle', title: 'dark-view' },
        { value: 'split', icon: 'graphline', title: 'split-view' }
      ],
      // Property that specifies if the name of the item will be displayed
      showName: true
    }
  }
}

export default preview
```
Now from the terminal run `yarn storybook` and play with the Theme switcher the above code will be understandable. We first add the `root` class to the main root element. To change the theme as stated earlier we will use the `light-theme` and `dark-theme` classes on our root element, we would change the theme in a similar way if are using our library in a Solid project.

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