DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

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

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

Step One: Badge styles & theming

We want to achieve the following for the Badge component -

<Badge colorScheme="success" isFlat isSquared size="md">
  Sample Badge
</Badge>
Enter fullscreen mode Exit fullscreen mode

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 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 -

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

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 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 -

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

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 -

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

The above code is pretty straightforward.

Step Three: Badge story

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

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 on how to create a theme switcher in storybook.

Under .storybook/preview.tsx file paste the following -

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

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. In the next tutorial we will create a theme able Button component. Until next time PEACE.

Top comments (0)