Building a design system with dark mode using React, Typescript, scss, cva and vite - Badge Component
Tags: react, css, typescript, vite
Published: 2023-03-21 06:03:44

### Introduction
This is part four of our series on building a complete design system from scratch. In the previous tutorial we created our `Box` & `Flex` component. 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://yaldram-react-scss-chakra-ui.s3-website.ap-south-1.amazonaws.com/?path=/story/atoms-badge--playground&globals=theme:split). All the code for this series is available on [GitHub](https://github.com/yaldram/react-scss-chakra-ui).

### Step One: Badge styles & theming
We want to achieve the following for the `Badge` component - 
```tsx
<Badge colorScheme="blue" variant="outline" size="md">
  Sample Badge
</Badge>
```
We have the `colorScheme` prop, taking values - red, orange, blue, purple, etc. We can pass 3 variants to the component - outline, solid, subtle. Basically, we have the `colorScheme` & `variant` combination. We also have 3 size variants - sm, md, lg.

For handling the light and dark modes we will use the data attribute selector `[data-theme="dark"]`, that means the root of our project should have the `data-theme="light"` or `data-theme="dark"` attribute. We will not use css variables for theming because chakra ui uses css in js and therefore it has a flat theme object, for the dark mode chakra ui is using javascript to create dark mode colors on the fly using rgba(), we will use `scss rgba()` function to achieve this.

The consumer of our library will use the attribute `data-theme="light"` or `data-theme="dark"` on its root element, we won't be creating any `ThemeProvider` because we don't need.

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

```scss
@use "sass:map";

/* base badge styles */
.badge {
  display: inline-flex;
  vertical-align: top;
  align-items: center;
  max-width: 100%;
  outline: 0;
  border-radius: 0.125rem;

  font-weight: $font-weight-semibold;
  line-height: $line-height-shorter;

  /* badge size sm */ 
  &.sm {
    min-height: $spacing-md;
    min-width: $spacing-md;
    font-size: $font-size-xs;
    padding-left: $spacing-xxs;
    padding-right: $spacing-xxs;
  }

  /* badge size md */ 
  &.md {
    min-height: $spacing-lg;
    min-width: $spacing-lg;
    font-size: $font-size-sm;
    padding-left: $spacing-xxs;
    padding-right: $spacing-xxs;
  }

  /* badge size lg */ 
  &.lg {
    min-height: $spacing-xl;
    min-width: $spacing-xl;
    font-size: $font-size-md;
    padding-left: $spacing-xs;
    padding-right: $spacing-xs;
  }

  @each $color in $color-schemes {
    $color-100: map.get($colors-map, #{$color + '100'});
    $color-200: map.get($colors-map, #{$color + '200'});
    $color-500: map.get($colors-map, #{$color + '500'});
    $color-800: map.get($colors-map, #{$color + '800'});

    /* badge variant solid with colorscheme & dark mode */ 
    &.solid.#{"" + $color} {
      background-color: $color-500;
      color: map.get($colors-map, "white");
      
      [data-theme="dark"] & {
        color: map.get($colors-map, 'whiteAlpha800');
        background-color: rgba($color-500, 0.6);
      }
    }

    /* badge variant outline with colorScheme & dark mode */ 
    &.outline.#{"" + $color} {
      color: $color-500;
      box-shadow: inset 0 0 0px 1px currentColor;
      
      [data-theme="dark"] & {
        color: rgba($color-200, 0.8);
      }
    }

    /* badge variant subtle with colorScheme & dark mode */ 
    &.subtle.#{"" + $color} {
      color: $color-800;
      background-color: $color-100;
      
      [data-theme="dark"] & {
        color: $color-200;
        background-color: rgba($color-200, 0.16);
      }
    }
  }
}
```

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 variant classes combined with the `colorScheme` - `.solid .red, .outline .red, .subtle .red`, we used scss map function to create all these combinations. 
4. We are mapping over the $color-schemes array and for each `colorScheme` we are picking the respective values from $colors-map our color pallet. Both $color-schemes & $colors-map are declared under `css/varaibles/_colors.scss`. 
5. For the variant classes we also need to handle `[data-theme="dark"]` condition in the css say if you pass `colorScheme` as `red` and `variant` as `solid` how will it look in dark mode.
6. So for the variants we are creating 2 classes, one for the light mode eg - `.red .outline` and one for the dark mode eg - `[data-theme="dark"] .red .outline`.

Notice one thing, we have not imported $color-schemes, $colors-map 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/css/variables/_fonts.scss" as *;
         @use "./src/css/variables/_spacings.scss" as *;
         @use "./src/css/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 React from "react";
import { cva, VariantProps } from "class-variance-authority";

import { ColorScheme } from "../../../cva-utils";
import { Box, BoxProps } from "../layouts";

import "./badge.scss";

const badge = cva(["badge"], {
  variants: {
    variant: {
      outline: "outline",
      solid: "solid",
      subtle: "subtle",
    },
    size: {
      sm: "sm",
      md: "md",
      lg: "lg",
    },
  },
  defaultVariants: {
    variant: "subtle",
    size: "md",
  },
});

export type BadgeProps = VariantProps<typeof badge> &
  BoxProps & {
    colorScheme?: ColorScheme;
  };

export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
  (props, ref) => {
    const {
      variant,
      size,
      colorScheme = "green",
      children,
      className,
      ...delegated
    } = props;

    const badgeClasses = badge({
      variant,
      size,
      className: [colorScheme, className].join(" "),
    });

    return (
      <Box ref={ref} className={badgeClasses} {...delegated}>
        {children}
      </Box>
    );
  }
);
```
The above code is pretty straightforward, we created the cva function along with the variants, then we created the `badgeClasses`. Finally, we pass the `badgeClasses` to the className prop.

### Step Three: Badge story
Under `atoms/badge` create a new file `badge.stories.tsx` - 
```tsx
import * as React from "react";
import { StoryObj } from "@storybook/react";

import { Badge, BadgeProps } from ".";
import { colorSchemes } from "../../../cva-utils";

export default {
  title: "Atoms/Badge",
};

export const Playground: StoryObj<BadgeProps> = {
  args: {
    variant: "subtle",
    colorScheme: "green",
    size: "md",
  },
  argTypes: {
    variant: {
      name: "variant",
      type: { name: "string", required: false },
      options: ["outline", "solid", "subtle"],
      description: "Variant for the Badge",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "subtle" },
      },
      control: {
        type: "select",
      },
    },
    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: ["sm", "md", "lg"],
      description: "Tag height width and horizontal padding",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "md" },
      },
      control: {
        type: "select",
      },
    },
  },
  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
import * as React from "react";

import { Flex, FlexProps } from "../src/components/atoms/layouts";

import "../src/css/main.scss";

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

function Container(props: FlexProps) {
  const { style, children, ...delegated } = props;

  return (
    <Flex
      align="start"
      p="md"
      style={{ minHeight: "100vh", ...style }}
      {...delegated}
    >
      {children}
    </Flex>
  );
}

const StoriesWithTheme = (StoryFun, context) => {
  const theme = context.parameters.theme || context.globals.theme;

  if (theme === "split") {
    return (
      <Flex>
        <Container bg="white" style={{ flexBasis: "50%" }} data-theme="light">
          <StoryFun />
        </Container>
        <Container bg="gray800" style={{ flexBasis: "50%" }} data-theme="dark">
          <StoryFun />
        </Container>
      </Flex>
    );
  }

  return (
    <Container bg={theme === "dark" ? "gray800" : "white"} data-theme={theme}>
      <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,
    },
  },
};

/**
 * This decorator is a global decorator will
 * be applied to each and every story
 */
export const decorators = [StoriesWithTheme];
```
Now from the terminal run `yarn storybook` and play with the Theme switcher the above code will be understandable. To change the theme as stated earlier we will use the data attribute `data-theme` on our root element, we would change the theme in a similar way if are using our library in our React projects.

Why are we not using a `ThemeProvider`? Because we don't need a `ThemeProvider`, the consumer of our library has to change the `data-theme` attribute on the root element of his project and handle theme in whichever way he wants store it in Redux, zustand, or Context. 

The consumer of our app can also create a `ThemeContext`. But never wrap your whole app inside a Context because if something updates the whole app will re-render. If say you want to hide some components in dark mode, you just wrap those components inside the context rather than wrapping the whole app.

### Conclusion
In this tutorial we created the first themeable component `Badge`. All the code for this tutorial can be found [here](https://github.com/yaldram/react-scss-chakra-ui). In the next tutorial we will create a spinner and some Icons. Until next time PEACE.