Building a design system with dark mode using React, Typescript, scss, cva and Vite - Alert Component
Tags: react, css, typescript, vite
Published: 2023-03-21 09:15:53

### Introduction
This is part six of our series on building a complete design system from scratch. In the previous tutorial created `Spinner` & `Icon` components. In this tutorial we will create a theme able `Alert` component with dark mode. 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/molecules-alert--playground&globals=theme:split). All the code for this series is available on [GitHub](https://github.com/yaldram/react-scss-chakra-ui).

### Step One: Alert styles & theming
We want to achieve the following for the `Alert` component - 
```tsx
<Alert variant="solid" status="warning">
  <AlertIcon />
  <Flex direction="col">
   <AlertTitle>Your browser is outdated!</AlertTitle>
   <AlertDescription>
     Your Chakra experience may be degraded.
    </AlertDescription>
   </Flex>
 </Alert>
```

We can also use the `Alert` component like so - 
```tsx
 <Alert variant="top-accent" colorScheme="red">
   <AlertIcon />
   There was an error processing your request.
  </Alert>
```

So similar to the `Badge` component we have a combination of `colorScheme` and `variant`. Also we have another prop `status` which can take values `info, status, warning, error` these we will internally translate to info - colorScheme is blue, error - colorScheme is red.

Also we need to create 4 different components `Alert`, `AlertIcon`, `AlertDescription` & `AlertTitle`. `Alert` component will take in all the props like status, variant and pass this information on to other components `AlertIcon` using React Context.

Under the `src/molecules` folder create a new folder called `alert`, under the `src/molecules/alert` folder create a `alert.scss` file -
```scss
@use "sass:map";

/* base alert styles */
.alert {
  --alert-icon-color: none;

  width: 100%;
  position: relative;
  overflow: hidden;
  padding: $spacing-sm;

  @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-white: map.get($colors-map, "white");
    $color-black: map.get($colors-map, "black");

    /* alert variant subtle, left-accent, top-accent common styles */ 
    &.subtle.#{"" + $color}, 
    &.top-accent.#{"" + $color},  
    &.left-accent.#{"" + $color}  {
      background-color: $color-100;
      color: $color-black;
      --alert-icon-color: #{$color-500};
      
      [data-theme="dark"] & {
        background-color: rgba($color-200, 0.16);
        color: $color-white;
        --alert-icon-color: #{$color-200};
      }
    }

    /* alert variant top-accent with colorscheme & dark mode */ 
    &.top-accent.#{"" + $color} {
      padding-inline-start: $spacing-md;
      border-top-width: 4px;
      border-top-style: solid;
      border-top-color: $color-500;

      [data-theme="dark"] & {
        border-top-color: $color-200;
      }
    }

    /* alert variant left-accent with colorscheme & dark mode */ 
    &.left-accent.#{"" + $color} {
      padding-inline-start: $spacing-md;
      border-left-width: 4px;
      border-left-style: solid;
      border-left-color: $color-500;
      
      [data-theme="dark"] & {
        border-left-color: $color-200;
      }
    }

    /* alert variant solid with colorscheme & dark mode */ 
    &.solid.#{"" + $color} {
      background-color: $color-500;
      color: $color-white;
      --alert-icon-color: #{$color-white};
      
      [data-theme="dark"] & {
        background-color: $color-200;
        color: map.get($colors-map, "gray900");
        --alert-icon-color: #{$color-black};
      }
    }
  }

  & > .alert-icon {
    color: var(--alert-icon-color);
  }
}

.alert-icon {
  display: inline;
  flex-shrink: 0;
  margin-inline-end: $spacing-md;
  width: 1.25rem;
  height: 1.5rem;
}

.alert-title {
  font-weight: $font-weight-semibold;
  line-height: $line-height-tall;
  margin-inline-end: $spacing-xxs;
}

.alert-description {
  display: inline;
  line-height: $line-height-taller;
}
```
- We first declared the styles for all the Alert components.
- Similar to the `Badge` component we create a combination of `colorScheme` and `variants`, we also included the dark mode styles.
- Take a look at the `.alert-icon` we want to control the `.alert-icon` color for some variant values, so I have used a css custom property and I am setting its value accordingly. 
- Take note that to assign a `scss variable` to a `css custom` property you have to use interpolation `--alert-icon-color: #{$color-black}`.

### Step Two: Alert component
Under `src/utils` folder create a new file `create-context.ts` -
```ts
import React from "react";

export interface CreateContextOptions {
  strict?: boolean;
  errorMessage?: string;
  name?: string;
}

type CreateContextReturn<T> = [React.Provider<T>, () => T, React.Context<T>];

export function createContext<ContextType>(options: CreateContextOptions = {}) {
  const {
    strict = true,
    errorMessage = "useContext: `context` is undefined. Seems you forgot to wrap component within the Provider",
    name,
  } = options;

  const componentContext = React.createContext<ContextType | undefined>(
    undefined
  );

  componentContext.displayName = name;

  function useContext() {
    const context = React.useContext(componentContext);

    if (!context && strict) {
      const error = new Error(errorMessage);
      error.name = "ContextError";
      Error.captureStackTrace?.(error, useContext);
      throw error;
    }

    return context;
  }

  return [
    componentContext.Provider,
    useContext,
    componentContext,
  ] as CreateContextReturn<ContextType>;
}
```
This is a generic function that will create the context, it will also create a hook to consume the context and it also has error handling built in. So if you want to create a context for the `Accordian`, `Tabs` use this funciton.

Now under `molecules/alert` create an `index.tsx` file -
```tsx
import * as React from "react";
import { cva, VariantProps } from "class-variance-authority";

import { Box, BoxProps, Flex, FlexProps } from "../../atoms/layouts";
import { InfoIcon, WarningIcon, CheckIcon } from "../../atoms/icons";
import { ColorScheme } from "../../../cva-utils";
import { createContext } from "../../../utils";

import "./alert.scss";

const STATUSES = {
  info: { icon: InfoIcon, colorScheme: "blue" },
  warning: { icon: WarningIcon, colorScheme: "orange" },
  success: { icon: CheckIcon, colorScheme: "green" },
  error: { icon: WarningIcon, colorScheme: "red" },
};

export type AlertStatus = keyof typeof STATUSES;

const alert = cva(["alert"], {
  variants: {
    variant: {
      subtle: "subtle",
      "left-accent": "left-accent",
      "top-accent": "top-accent",
      solid: "solid",
    },
  },
  defaultVariants: {
    variant: "subtle",
  },
});

type AlertVariant = VariantProps<typeof alert>["variant"];

interface AlertContext {
  status: AlertStatus;
  variant: AlertVariant;
  colorScheme: ColorScheme;
}

const [AlertProvider, useAlertContext] = createContext<AlertContext>({
  name: "AlertContext",
  errorMessage:
    "useAlertContext: `context` is undefined. Seems you forgot to wrap alert components in `<Alert />`",
});

interface AlertOptions {
  status?: AlertStatus;
}

export interface AlertProps
  extends Omit<FlexProps, "bg" | "backgroundColor">,
    AlertOptions {
  colorScheme?: ColorScheme;
  variant?: AlertVariant;
}

export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
  (props, ref) => {
    const { status = "info", variant, align = "center", ...delegated } = props;

    const colorScheme =
      delegated.colorScheme ?? (STATUSES[status].colorScheme as ColorScheme);

    const alertClasses = alert({
      variant,
      className: colorScheme,
    });

    return (
      <AlertProvider value={{ status, variant, colorScheme }}>
        <Flex
          ref={ref}
          role="alert"
          align="center"
          className={alertClasses}
          {...delegated}
        />
      </AlertProvider>
    );
  }
);

export interface AlertTitleProps extends BoxProps {}

const alertTitle = cva(["alert-title"]);

export function AlertTitle(props: AlertTitleProps) {
  const { children, className, ...delegated } = props;

  return (
    <Box className={alertTitle({ className })} {...delegated}>
      {children}
    </Box>
  );
}

export interface AlertDescriptionProps extends BoxProps {}

const alertDescription = cva(["alert-description"]);

export function AlertDescription({
  className,
  ...delegated
}: AlertDescriptionProps) {
  return <Box className={alertDescription({ className })} {...delegated} />;
}

export interface AlertIconProps extends BoxProps {}

const alertIcon = cva(["alert-icon"]);

export function AlertIcon(props: AlertIconProps) {
  const { status, colorScheme } = useAlertContext();
  const { colorScheme: statusColorScheme, icon: BaseIcon } = STATUSES[status];

  const iconColorScheme = colorScheme ?? statusColorScheme;

  const alertIconClasses = alertIcon({
    className: iconColorScheme,
  });

  return (
    <span className={alertIconClasses} {...props}>
      <BaseIcon />
    </span>
  );
} 
```
The above code is pretty straightforward, I would suggest you read it carefully and play around with the `Alert` component in storybook. Let me know if you have any questions.

### Step Three: Alert Stories
Under `molecules/alert` create a new file `alert.stories.tsx` and paste the following code - 
```tsx
import React from "react";
import { StoryObj } from "@storybook/react";

import { Alert, AlertIcon, AlertDescription, AlertTitle, AlertProps } from ".";
import { Flex } from "../../atoms/layouts";
import { colorSchemes } from "../../../cva-utils";

export default {
  title: "Molecules/Alert",
};

export const Playground: StoryObj<AlertProps> = {
  args: {
    colorScheme: "gray",
    variant: "solid",
  },
  argTypes: {
    colorScheme: {
      name: "colorScheme",
      type: { name: "string", required: false },
      options: colorSchemes,
      description: "The Color Scheme for the button",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "gray" },
      },
      control: {
        type: "select",
      },
    },
    variant: {
      name: "variant",
      type: { name: "string", required: false },
      options: ["solid", "subtle", "left-accent", "top-accent"],
      description: "The variant of the alert",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "solid" },
      },
      control: {
        type: "select",
      },
    },
  },
  render: (args: AlertProps) => (
    <Alert {...args}>
      <AlertIcon />
      There was an error processing your request
    </Alert>
  ),
};

export const AlertStatus: StoryObj<AlertProps> = {
  args: {
    status: "info",
    variant: "subtle",
  },
  argTypes: {
    status: {
      name: "status",
      type: { name: "string", required: false },
      options: ["info", "warning", "success", "error"],
      description: "The status of the alert",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "status" },
      },
      control: {
        type: "select",
      },
    },
    variant: {
      name: "variant",
      type: { name: "string", required: false },
      options: ["solid", "subtle", "left-accent", "top-accent"],
      description: "The variant of the alert",
      table: {
        type: { summary: "string" },
        defaultValue: { summary: "solid" },
      },
      control: {
        type: "select",
      },
    },
  },
  render: (args: AlertProps) => (
    <Alert {...args}>
      <AlertIcon />
      <Flex direction="col">
        <AlertTitle>Your browser is outdated!</AlertTitle>
        <AlertDescription>
          Your Chakra experience may be degraded.
        </AlertDescription>
      </Flex>
    </Alert>
  ),
};
```
From the terminal run `yarn storybook` and check the output.

### Conclusion
In this tutorial we created the second theme able component `Alert`. All the code for this tutorial can be found [here](https://github.com/yaldram/react-scss-chakra-ui). In the next tutorial we will work on the `Button` component. Until next time PEACE.