Next-Level Theming: Creating Multi-Themed Apps with Tailwind CSS, CSS Variables & React
Tags: tailwindcss, css, react, design
Published: 2023-08-31 12:37:57

### Introduction
Unlocking the potential of theming in UI design has never been more exciting, especially with shining examples like [shadcn/ui](https://ui.shadcn.com/themes). With its array of captivating themes—ranging from blue, red, orange I found myself intrigued by the challenge: How could I create such diverse themes? In this tutorial, we'll embark on a thematic journey through 3 distinct approaches. We will create a  theme able button component with the help of [Tailwind Variants](https://www.tailwind-variants.org/) for both light and dark modes, all while harnessing the prowess of Tailwind CSS.

Our aim is to design a versatile Button component with various options like solid and outline styles, available in multiple color schemes such as red, orange, and green. This component should seamlessly function in both light and dark themes. I recommend checking out the [github repository](https://github.com/yaldram/tw-variants) and reviewing the attached screenshots in the readme section. To achieve distinct `Button` styles, we're utilizing [Tailwind Variants](https://www.tailwind-variants.org/), though this specific aspect will be covered in upcoming tutorials.

Spanning across 3 branches of [my repository](https://github.com/yaldram/tw-variants) - 
- In the [first branch](https://github.com/yaldram/tw-variants/tree/feat/chakra-ui) drawing inspiration from [Chakra UI](https://chakra-ui.com/docs/components/button/usage), we'll wield theme tokens to craft our adaptable button, seamlessly transitioning between light and dark modes, using the Tailwind `:dark` selector. 
- In the [second branch](https://github.com/yaldram/tw-variants/tree/feat/next-ui) taking inspiration from [nextui's] (https://nextui.org/docs/components/button) separate tokens for light and dark mode colors, we will use the power of CSS variables with Tailwind CSS.
- In the [final branch](https://github.com/yaldram/tw-variants/tree/feat/shad-ui), taking inspiration from [shadcn/ui](https://ui.shadcn.com/themes) kaleidoscope of themes - red, blue, orange, green, all with their dark mode counterparts, we will put Tailwind CSS and CSS variables into awe-inspiring action.

### Project Setup
We will be using vite to bootstrap a React project. From your terminal run - 
```bash
 npm create vite@latest
```
During the setup, you'll be prompted to provide a project name. Feel free to choose a name that resonates with you. Select `React` as the framework and `TypeScript` as the variant. Now we need to install & setup Tailwind CSS, I would recommend you follow this [detailed guide](https://tailwindcss.com/docs/guides/vite). Finally we will install Tailwind Variants for creating multi-variant React component - 
```bash
 yarn add tailwind-variants
```
With that you can commit your code, our project setup is completed.

### Charka UI style theming 
In this section, we'll embark on our theming journey by simulating Chakra UI's elegant theming approach, but using Tailwind CSS. First, create a new Git branch -
```bash
git checkout -b chakra-ui
```

![chakra-ui](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sq6paaiouv1s87evej53.png)
We're aiming to create a button similar to the image above. To use this button, you'd simply employ the following code snippet for your component API:
```tsx
<Button
  size="lg"
  colorScheme="orange"
  variant="outline"
>
  Click Me
</Button>
```
This structure enhances code readability and offers a user-friendly way to generate the desired button appearance.

Open the index.css file and introduce custom color tokens using CSS variables. These tokens will serve as the foundation for our theme, you can get the complete code [here](https://github.com/yaldram/tw-variants/blob/feat/chakra-ui/src/index.css) - 
```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --gray-50: 247, 250, 252;
    --gray-100: 237, 242, 247;
    --gray-200: 226, 232, 240;
    --gray-300: 203, 213, 224;
    --gray-400: 160, 174, 192;
    --gray-500: 113, 128, 150;
    --gray-600: 74, 85, 104;
    --gray-700: 45, 55, 72;
    --gray-800: 26, 32, 44;
    --gray-900: 23, 25, 35;

    --red-50: 255, 245, 245;
    --red-100: 254, 215, 215;
    --red-200: 254, 178, 178;
    --red-300: 252, 129, 129;
    --red-400: 245, 101, 101;
    --red-500: 229, 62, 62;
    --red-600: 197, 48, 48;
    --red-700: 155, 44, 44;
    --red-800: 130, 39, 39;
    --red-900: 99, 23, 27;

   ...other tokens
  }
}
```
The `@layer base {}` directive is used to define foundational styles applied globally. In theming, it's employed to set up color, spacing, fonts tokens universally, ensuring a consistent theme palette throughout the application.

Now in the tailwind.config.js paste the following - 
```js
/** @type {import('tailwindcss').Config} */
import { withTV } from 'tailwind-variants/transformer'

const getPropertyValue = (variable) => {
  return ({ opacityValue }) =>
    opacityValue
      ? `rgba(var(--${variable}), ${opacityValue})`
      : `rgb(var(--${variable}))`
}


export default withTV({
  darkMode: "class",
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      gray: {
        50: getPropertyValue('gray-50'),
        100: getPropertyValue('gray-100'),
        200: getPropertyValue('gray-200'),
        300: getPropertyValue('gray-300'),
        400: getPropertyValue('gray-400'),
        500: getPropertyValue('gray-500'),
        600: getPropertyValue('gray-600'),
        700: getPropertyValue('gray-700'),
        800: getPropertyValue('gray-800'),
        900: getPropertyValue('gray-900')
      },
      red: {
        50: getPropertyValue('red-50'),
        100: getPropertyValue('red-100'),
        200: getPropertyValue('red-200'),
        300: getPropertyValue('red-300'),
        400: getPropertyValue('red-400'),
        500: getPropertyValue('red-500'),
        600: getPropertyValue('red-600'),
        700: getPropertyValue('red-700'),
        800: getPropertyValue('red-800'),
        900: getPropertyValue('red-900')
      },
      orange: {
        50: getPropertyValue('orange-50'),
        100: getPropertyValue('orange-100'),
        200: getPropertyValue('orange-200'),
        300: getPropertyValue('orange-300'),
        400: getPropertyValue('orange-400'),
        500: getPropertyValue('orange-500'),
        600: getPropertyValue('orange-600'),
        700: getPropertyValue('orange-700'),
        800: getPropertyValue('orange-800'),
        900: getPropertyValue('orange-900')
      },
      green: {
        50: getPropertyValue('green-50'),
        100: getPropertyValue('green-100'),
        200: getPropertyValue('green-200'),
        300: getPropertyValue('green-300'),
        400: getPropertyValue('green-400'),
        500: getPropertyValue('green-500'),
        600: getPropertyValue('green-600'),
        700: getPropertyValue('green-700'),
        800: getPropertyValue('green-800'),
        900: getPropertyValue('green-900')
      },
    },
    spacing: {
      xxs: "0.5rem",
      xs: "0.8rem",
      sm: "1rem",
      md: "1.25rem",
      lg: "1.5rem",
      xl: "2rem",
      xxl: "2.4rem",
      "3xl": "3rem",
      "4xl": "3.6rem"
    }
  },
  plugins: [],
})
```
First, we're expanding our theme to make sure that when we use Tailwind classes like `text-red-600`, we're using our custom colors, not the default Tailwind colors. We've also introduced tokens for spacing. Now, the interesting part is the `getPropertyValue` function. We're using it because when Tailwind generates classes for `bg-green-500`, it sets an opacity value like - 
```css
 .bg-green-500 {  
   --tw-bg-opacity: 1;
    background-color: rgb(34 197 94 / var(--tw-bg-opacity)); 
}
```

The `getPropertyValue` function helps us generate the right color dynamically by allowing us to pass opacity values. For instance, using the class `bg-green-500` alongside `opacity-50` will adjust the color's transparency, as the function smartly handles opacity values for us. **And for the same reason we are using rgb values for our color tokens**, so that it becomes easy for us to add the opacity variable.

Under `src` folder create the `Button.tsx` file, using Tailwind variants, we'll craft our `Button` component.  - 
```tsx
import { VariantProps, tv } from "tailwind-variants";

const baseButton = tv({
  base: "border-none outline-none cursor-pointer inline-flex items-center justify-center px-[0.25em] py-[0.75em] font-semibold text-center leading-[1.1] transition duration-220 ease-in-out rounded-[0.375rem] focus:shadow-outline hover:bg-transparent hover:bg-initial disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none",
  variants: {
    colorScheme: {
      red: "",
      orange: "",
      green: "",
    },
    variant: {
      solid: "",
      outline: "",
    },
  },
  compoundVariants: [
    {
      colorScheme: "green",
      variant: "solid",
      class:
        "bg-green-500 dark:bg-green-200 text-white dark:text-gray-800 hover:bg-green-600 dark:hover:bg-green-300 hover:disabled:bg-green-500 dark:hover:disabled:bg-green-200 active:bg-green-700 dark:active:bg-green-400",
    },
    {
      colorScheme: "red",
      variant: "solid",
      class:
        "bg-red-500 dark:bg-red-200 text-white dark:text-gray-800 hover:bg-red-600 dark:hover:bg-red-300  hover:disabled:bg-red-500 dark:hover:disabled:bg-red-200 active:bg-red-700 dark:active:bg-red-400",
    },
    {
      colorScheme: "orange",
      variant: "solid",
      class:
        "bg-orange-500 dark:bg-orange-200 text-white dark:text-gray-800 hover:bg-orange-600 dark:hover:bg-orange-300 hover:disabled:bg-orange-500 dark:hover:disabled:bg-orange-200 active:bg-orange-700 dark:active:bg-orange-400",
    },
    {
      colorScheme: "green",
      variant: "outline",
      class:
        "text-green-600 dark:text-green-200 bg-transparent border-solid border border-current hover:bg-green-50 dark:hover:bg-green-200/[.12] active:bg-green-100 dark:active:bg-green-200/[.24]",
    },
    {
      colorScheme: "red",
      variant: "outline",
      class:
        "text-red-600 dark:text-red-200 bg-transparent border-solid border border-current hover:bg-red-50 dark:hover:bg-red-200/[.12] active:bg-red-100 dark:active:bg-red-200/[.24]",
    },
    {
      colorScheme: "orange",
      variant: "outline",
      class:
        "text-orange-600 dark:text-orange-200 bg-transparent border-solid border border-current hover:bg-orange-50 dark:hover:bg-orange-200/[.12] active:bg-orange-100 dark:active:bg-orange-200/[.24]",
    },
  ],
  defaultVariants: {
    colorScheme: "green",
    variant: "solid",
  },
});

const button = tv(
  {
    extend: baseButton,
    variants: {
      size: {
        xs: "h-[1.5rem] min-w-[1.5rem] text-xs px-xs",
        sm: "h-[2rem] min-w-[2rem] text-sm px-sm",
        md: "h-[2.5rem] min-w-[2.5rem] text-md px-md",
        lg: "h-[3rem] min-w-[3rem] text-lg px-lg",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
  {
    responsiveVariants: true,
  }
);

export type ButtonProps = VariantProps<typeof button> &
  React.ComponentPropsWithoutRef<"button">;

export function Button(props: ButtonProps) {
  const { colorScheme, size, variant, className, ...delegated } = props;
  return (
    <button
      className={button({
        colorScheme,
        variant,
        className,
        size,
      })}
      {...delegated}
    />
  );
}
```
Notice that we're utilizing the `dark:` selector for managing dark mode effortlessly – Tailwind makes adding these selectors incredibly simple.

Finally test the button component under `App.tsx`, you can get the complete code [here](https://github.com/yaldram/tw-variants/blob/feat/chakra-ui/src/App.tsx). To toggle the theme from light to dark add `.light` / `.dark` class to the parent element.

### Next UI style theming 
Picture a scenario where you write your styles just once, but they seamlessly adjust to both light and dark themes. Say goodbye to duplicating styles using the `dark:`selector for each property. This is where the magic of separate tokens for each theme mode comes in. By employing this method, your styles dynamically adapt as you switch themes, eradicating redundancy and streamlining your styling process. 
![next-ui](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5v4gmv6z2cm85iqbdk37.png)
We're aiming to create a button similar to the image above. First, create a new Git branch -
```bash
git checkout -b next-ui
```

Open the index.css file and introduce custom color tokens using CSS variables. These tokens will serve as the foundation for our theme, you can get the complete code [here](https://github.com/yaldram/tw-variants/blob/feat/next-ui/src/index.css) - 
```css
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  .light {
    --blue50: 237, 245, 255;
    --blue100: 225, 239, 255;
    --blue200: 206, 228, 254;
    --blue300: 183, 213, 248;
    --blue400: 150, 193, 242;
    --blue500: 94, 162, 239;
    --blue600: 0, 114, 245;
    --blue700: 0, 95, 204;
    --blue800: 0, 71, 153;
    --blue900: 0, 37, 77;
 
    ...other tokens
  }

  .dark {
    --blue50: 16, 37, 62;
    --blue100: 16, 44, 76;
    --blue200: 15, 49, 88;
    --blue300: 13, 56, 104;
    --blue400: 10, 66, 129;
    --blue500: 9, 82, 165;
    --blue600: 0, 114, 245;
    --blue700: 54, 148, 255;
    --blue800: 54, 148, 255;
    --blue900: 234, 244, 255;

    ...other tokens
   }
}

@layer utilities {
  .shadow-primary {
    box-shadow: 0 4px 14px 0 rgb(var(--blue500));
  }

  .shadow-secondary {
    box-shadow: 0 4px 14px 0 rgb(var(--purple500));
  }

  .shadow-error {
    box-shadow: 0 4px 14px 0 rgb(var(--red500));
  }

  .shadow-success {
    box-shadow: 0 4px 14px 0 rgb(var(--green500));
  }

  .shadow-warning {
    box-shadow: 0 4px 14px 0 rgb(var(--yellow500));
  }
}
```
Unlike the earlier method, we're now generating tokens for both the light and dark themes. This means that when the theme changes, the appropriate token for the corresponding mode automatically takes effect, eliminating the need for the `dark:` selector. Additionally, by including custom classes in the utilities layer, we've opened the door to extending Tailwind classes. This is particularly useful when we require classes like `shadow-primary`, which aren't native to Tailwind. This approach not only optimizes theme switching but also allows for seamless expansion of the styling capabilities.

Now in the tailwind.config.js, copy paste the code from [here](https://github.com/yaldram/tw-variants/blob/feat/next-ui/tailwind.config.js) - 
```js
theme: {
  extend: {
    colors: {
      'primary': {
        'light': getPropertyValue('blue200'),
        'light-hover': getPropertyValue('blue300'),
        'light-active': getPropertyValue('blue400'),
        'light-contrast': getPropertyValue('blue600'),
        'border': getPropertyValue('blue500'),
        'border-hover': getPropertyValue('blue600'),
        'solid-hover': getPropertyValue('blue700'),
        'shadow': getPropertyValue('blue500'),
        'DEFAULT': getPropertyValue('blue600'),
      },

      'secondary': {
        'light': getPropertyValue('purple200'),
        'light-hover': getPropertyValue('purple300'),
        'light-active': getPropertyValue('purple400'),
        'light-contrast': getPropertyValue('purple600'),
        'border': getPropertyValue('purple500'),
        'border-hover': getPropertyValue('purple600'),
        'solid-hover': getPropertyValue('purple700'),
        'shadow': getPropertyValue('purple500'),
        'DEFAULT': getPropertyValue('purple600'),
      },
      // ... (other tokens)
    }
  }
}       
```
In our Tailwind config, we've introduced `semantic tokens` that add a layer of organization above the foundational base tokens. This approach offers several advantages. For instance, consider the primary color definition. When the mode is light, it draws from the light theme, while in dark mode, it derives from the corresponding base token associated with the dark theme. This dynamic behavior ensures that regardless of the mode, the primary color consistently adapts, maintaining the expected appearance while switching between light and dark themes. 

Now under `src/Button.tsx` paste the following - 
```tsx
import { VariantProps, tv } from "tailwind-variants";

const baseButton = tv({
  base: "appearance-none box-border flex items-center justify-center leading-5 select-none text-center whitespace-nowrap border-none cursor-pointer transition duration-250 ease-in border-2 active:scale-[.97]",
  variants: {
    colorScheme: {
      primary: "",
      secondary: "",
      success: "",
      warning: "",
      error: "",
    },
    variant: {
      solid: "",
      bordered: "bg-transparent border-2 border-solid",
      ghost: "bg-transparent border-2 border-solid",
      flat: "",
    },
    isShadow: {
      true: "",
    },
  },
  compoundVariants: [
    {
      colorScheme: "primary",
      variant: "solid",
      class: "bg-primary text-white",
    },
    {
      colorScheme: "secondary",
      variant: "solid",
      class: "bg-secondary text-white",
    },
    {
      colorScheme: "success",
      variant: "solid",
      class: "bg-success text-black",
    },
    {
      colorScheme: "warning",
      variant: "solid",
      class: "bg-warning text-black",
    },
    {
      colorScheme: "error",
      variant: "solid",
      class: "bg-error text-white",
    },

    {
      colorScheme: "primary",
      isShadow: true,
      class: "shadow-primary",
    },
    {
      colorScheme: "secondary",
      isShadow: true,
      class: "shadow-secondary",
    },
    {
      colorScheme: "success",
      isShadow: true,
      class: "shadow-success",
    },
    {
      colorScheme: "warning",
      isShadow: true,
      class: "shadow-warning",
    },
    {
      colorScheme: "error",
      isShadow: true,
      class: "shadow-error",
    },

    {
      colorScheme: "primary",
      variant: "bordered",
      class: "text-primary border-primary",
    },
    {
      colorScheme: "secondary",
      variant: "bordered",
      class: "text-secondary border-secondary",
    },
    {
      colorScheme: "success",
      variant: "bordered",
      class: "text-success border-success",
    },
    {
      colorScheme: "warning",
      variant: "bordered",
      class: "text-warning border-warning",
    },
    {
      colorScheme: "error",
      variant: "bordered",
      class: "text-error border-error",
    },

    {
      colorScheme: "primary",
      variant: "ghost",
      class: "text-primary border-primary hover:text-white hover:bg-primary",
    },
    {
      colorScheme: "secondary",
      variant: "ghost",
      class:
        "text-secondary border-secondary hover:text-white hover:bg-secondary",
    },
    {
      colorScheme: "success",
      variant: "ghost",
      class: "text-success border-success hover:text-black hover:bg-success",
    },
    {
      colorScheme: "warning",
      variant: "ghost",
      class: "text-warning border-warning hover:text-black hover:bg-warning",
    },
    {
      colorScheme: "error",
      variant: "ghost",
      class: "text-error border-error hover:text-white hover:bg-error",
    },

    {
      colorScheme: "primary",
      variant: "flat",
      class:
        "bg-primary-light text-primary-light-contrast hover:bg-primary-light-hover active:bg-primary-light-active",
    },
    {
      colorScheme: "secondary",
      variant: "flat",
      class:
        "bg-secondary-light text-secondary-light-contrast hover:bg-secondary-light-hover active:bg-secondary-light-active",
    },
    {
      colorScheme: "success",
      variant: "flat",
      class:
        "bg-success-light text-success-light-contrast hover:bg-success-light-hover active:bg-success-light-active",
    },
    {
      colorScheme: "warning",
      variant: "flat",
      class:
        "bg-warning-light text-warning-light-contrast hover:bg-warning-light-hover active:bg-warning-light-active",
    },
    {
      colorScheme: "error",
      variant: "flat",
      class:
        "bg-error-light text-error-light-contrast hover:bg-error-light-hover active:bg-error-light-active",
    },
  ],
  defaultVariants: {
    colorScheme: "primary",
    variant: "solid",
  },
});

const button = tv(
  {
    extend: baseButton,
    variants: {
      size: {
        xs: "rounded-xs h-10 pl-3 pr-3 leading-10 min-w-20 text-xs",
        sm: "rounded-sm h-12 pl-5 pr-5 leading-14 min-w-36 text-sm",
        md: "rounded-md h-14 pl-7 pr-7 leading-14 min-w-48 text-sm",
        lg: "rounded h-16 pl-9 pr-9 leading-15 min-w-60 text-md",
        xl: "rounded-xl h-18 pl-10 pr-10 leading-17 min-w-72 text-lg",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
  {
    responsiveVariants: true,
  }
);

export type ButtonProps = VariantProps<typeof button> &
  React.ComponentPropsWithoutRef<"button">;

export function Button(props: ButtonProps) {
  const { colorScheme, size, variant, isShadow, className, ...delegated } =
    props;
  return (
    <button
      className={button({
        colorScheme,
        variant,
        className,
        isShadow,
        size,
      })}
      {...delegated}
    />
  );
}
```
As evident from the above example, the absence of the `dark:` selector is notable. Our semantic tokens, like primary and secondary, smoothly manage theming shifts. This feature enhances code readability and maintenance significantly. By allowing these tokens to handle the theming intricacies, our code becomes more concise and easier to understand, resulting in streamlined development and maintenance processes.

Finally test the button component under `App.tsx`, you can get the complete code [here](https://github.com/yaldram/tw-variants/blob/feat/next-ui/src/App.tsx). To toggle the theme from light to dark add `.light` / `.dark` class to the parent element.

### Shadcn style theming 
Up to this point, we've constructed a single theme encompassing light and dark modes. But can we emulate [shadcn's](https://ui.shadcn.com/themes) approach? They feature multiple themes — red, green, blue, orange - each accompanied by its respective dark mode. Could this method uphold code quality and readability as effectively?
![shadcn](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g927g5bize5r8wq5r6lb.png)
First, create a new Git branch -
```bash
git checkout -b shad-ui
```
First open the `index.css` file and add the CSS variables - 
```css
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Red Theme */
.red-theme {
  --primary: 346.8 77.2% 49.8%;
  --primary-foreground: 355.7 100% 97.3%;
  --secondary: 240 4.8% 95.9%;
  --secondary-foreground: 240 5.9% 10%;
}

.dark .red-theme {
  --primary: 346.8 77.2% 49.8%;
  --primary-foreground: 355.7 100% 97.3%;
  --secondary: 240 3.7% 15.9%;
  --secondary-foreground: 0 0% 98%;
}

/* Blue Theme */
.blue-theme {
  --primary: 221.2 83.2% 53.3%;
  --primary-foreground: 210 40% 98%;
  --secondary: 210 40% 96.1%;
  --secondary-foreground: 222.2 47.4% 11.2%;
}

.dark .blue-theme {
  --primary: 217.2 91.2% 59.8%;
  --primary-foreground: 222.2 47.4% 11.2%;
  --secondary: 217.2 32.6% 17.5%;
  --secondary-foreground: 210 40% 98%;
}

/* Green Theme */
.green-theme {
  --primary: 142.1 76.2% 36.3%;
  --primary-foreground: 355.7 100% 97.3%;
  --secondary: 240 4.8% 95.9%;
  --secondary-foreground: 240 5.9% 10%;
}

.dark .green-theme {
  --primary: 142.1 70.6% 45.3%;
  --primary-foreground: 144.9 80.4% 10%;
  --secondary: 240 3.7% 15.9%;
  --secondary-foreground: 0 0% 98%;
}

/* Orange Theme */
.orange-theme {
  --primary: 24.6 95% 53.1%;
  --primary-foreground: 60 9.1% 97.8%;
  --secondary: 60 4.8% 95.9%;
  --secondary-foreground: 24 9.8% 10%;
}

.dark .orange-theme {
  --primary: 20.5 90.2% 48.2%;
  --primary-foreground: 60 9.1% 97.8%;
  --secondary: 12 6.5% 15.1%;
  --secondary-foreground: 60 9.1% 97.8%;
}
```
We're defining CSS variables to represent different theme colors, both for regular and dark modes. These variables ensure that the themes can be conveniently customized and managed. The dark class is applied when the dark mode is active, which updates the variable values accordingly.

Next open the `tailwind.config.js` file and create the semantic tokens - 
```js
/** @type {import('tailwindcss').Config} */

const getPropertyValue = (variable) => {
  return ({ opacityValue }) =>
    opacityValue
      ? `hsl(var(--${variable}) / ${opacityValue})`
      : `hsl(var(--${variable}))`
}


export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: getPropertyValue("primary"),
          foreground: getPropertyValue("primary-foreground")
        },
        secondary: {
          DEFAULT: getPropertyValue("secondary"),
          foreground: getPropertyValue("secondary-foreground")
        }
      }
    },
    spacing: {
      xxs: '0.6rem',
      xs: '0.8rem',
      sm: '1rem',
      md: '1.2rem',
      lg: '1.5rem',
      xl: '2rem',
      xxl: '2.4rem',
      '3xl': '3rem',
      '4xl': '3.6rem'
    }
  },
  plugins: [],
}
```
Now under `src/Button.tsx` - 
```tsx
import { VariantProps, tv } from "tailwind-variants";

const baseButton = tv({
  base: "border-none outline-none cursor-pointer inline-flex items-center justify-center px-[0.25em] py-[0.75em] text-center leading-[1.1] transition duration-220 ease-in-out rounded-[0.375rem] focus:shadow-outline hover:bg-transparent hover:bg-initial disabled:opacity-40 disabled:cursor-not-allowed disabled:shadow-none",
  variants: {
    colorScheme: {
      primary: "",
    },
    varaint: {
      solid: "",
      outline: "",
    },
  },
  compoundVariants: [
    {
      colorScheme: "primary",
      varaint: "solid",
      class: "bg-primary text-primary-foreground hover:bg-primary/90",
    },

    {
      colorScheme: "primary",
      varaint: "outline",
      class:
        "bg-transparent text-primary hover:bg-primary/10 border-solid border border-primary",
    },
  ],
  defaultVariants: {
    colorScheme: "primary",
    varaint: "solid",
  },
});

const button = tv(
  {
    extend: baseButton,
    variants: {
      size: {
        xs: "h-[1.5rem] min-w-[1.5rem] text-xs px-xs",
        sm: "h-[2rem] min-w-[2rem] text-sm px-sm",
        md: "h-[2.5rem] min-w-[2.5rem] text-md px-md",
        lg: "h-[3rem] min-w-[3rem] text-lg px-lg",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
  {
    responsiveVariants: true,
  }
);

export type ButtonProps = VariantProps<typeof button> &
  React.ComponentPropsWithoutRef<"button">;

export function Button(props: ButtonProps) {
  const { colorScheme, size, varaint, className, ...delegated } = props;
  return (
    <button
      className={button({
        colorScheme,
        varaint,
        className,
        size,
      })}
      {...delegated}
    />
  );
}
```
Finally test the button component under `App.tsx`, you can get the complete code [here](https://github.com/yaldram/tw-variants/blob/feat/shad-ui/src/App.tsx). To toggle the theme from light to dark add `.light` / `.dark` class to the parent element.

### Conclusion
We explored multiple theming approaches throughout this journey. Initially, we employed flat tokens and the dark selector. In the second approach, we refined the code, establishing distinct tokens for light and dark modes, and introduced an abstraction layer through semantic tokens. Our explorations culminated in the creation of multiple themes with their corresponding dark modes. This intricate process, while it may seem complex, is surprisingly straightforward.

All the code can be found [here](https://github.com/yaldram/tw-variants). Until next time PEACE.