Build Chakra UI Alert component using react, typescript, styled-components and styled-system
Tags: react, typescript
Published: 2021-09-05 16:39:42

### Introduction

Let us continue building our chakra components using `styled-components` & `styled-system`. In this tutorial we will be cloning the Chakra UI `Alert` component.

- I would like you to first check the chakra docs for [alert](https://chakra-ui.com/docs/feedback/alert).
- All the code for this tutorial can be found under the **molecule-alert** branch [here](https://github.com/yaldram/chakra-ui-clone).

### Prerequisite

Please check the Chakra Alert Component code [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/alert/src/alert.tsx). The theme / styles are [here](https://github.com/chakra-ui/chakra-ui/blob/main/packages/theme/src/components/alert.ts) In this tutorial we will -

- Create an Alert component.
- Create story for the Alert component.

### Setup

- First let us create a branch, from the main branch run -

```bash
git checkout -b molecule-alert
```

- Under the `components/molecules` folder create a new folder called `alert`. Under alert folder create 2 files index.tsx and alert.stories.tsx.

- So our folder structure stands like - **src/components/molecules/alert**.

### Alert Context

- Our Alert component has multiple variants like solid, subtle, left-accent, top-accent and we have multiple components that make up our Alert component like `AlertIcon`, `AlertTitle`, `AlertDescription`.

- You can see from the chakra docs that when you change the alert variant, from solid to say subtle the `AlertIcon` changes its color. There has to be some mechanism that passes this information from the parent to the `AlertIcon`. We will be using react context for this purpose.

- Now chakra is awesome, traditionally we create a context and create a hook to consume the context values. Chakra does this in an elegant way which I will use for every project of mine.

- Under the `utils` folder create a new file called `context.ts` and paste the following code -

```ts
import * as 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,
    // eslint-disable-next-line max-len
    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>;
}
```

- `createContext` function creates a named context, provider, and hook.

### Alert Component

- If you have followed this series the code for this tutorial is pretty self-explanatory. Under the `alert/index.tsx` file paste the following code -

```tsx
import * as React from "react";
import styled from "styled-components";
import { variant as variantFun } from "styled-system";
import { ColorScheme } from "../../../theme/colors";

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

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;

export type AlertVariants = "subtle" | "left-accent" | "top-accent" | "solid";

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

const [AlertProvider, useAlertContext] = createContext<AlertContext>({
  name: "AlertContext",
  errorMessage:
    // eslint-disable-next-line max-len
    "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?: AlertVariants;
}

const alertVariants = (colorScheme: ColorScheme) => {
  const backgroundColor = `${colorScheme}100`;
  const borderColor = `${colorScheme}500`;

  return {
    subtle: {
      backgroundColor,
    },
    solid: {
      backgroundColor: borderColor,
      color: "white",
    },
    "left-accent": {
      paddingStart: "md",
      borderLeftWidth: "4px",
      borderLeftStyle: "solid",
      borderLeftColor: borderColor,
      backgroundColor,
    },
    "top-accent": {
      paddingTop: "md",
      borderTopWidth: "4px",
      borderTopStyle: "solid",
      borderTopColor: borderColor,
      backgroundColor,
    },
  };
};

const AlertContainer = styled(Flex)<AlertProps>`
  width: 100%;
  position: relative;
  overflow: hidden;

  ${({ colorScheme = "gray" }) =>
    variantFun({
      prop: "variant",
      variants: alertVariants(colorScheme),
    })}
`;

export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
  (props, ref) => {
    const {
      status = "info",
      variant = "subtle",
      align = "center",
      ...delegated
    } = props;
    const colorScheme =
      props.colorScheme ?? (STATUSES[status].colorScheme as ColorScheme);

    return (
      <AlertProvider value={{ status, variant, colorScheme }}>
        <AlertContainer
          role="alert"
          ref={ref}
          variant={variant}
          p="sm"
          align={align}
          colorScheme={colorScheme}
          {...delegated}
        />
      </AlertProvider>
    );
  }
);
```

- We first created a map for our Alert statuses along with the icon to be used.

- Next we created the Alert Context to which we will pass status, variant, colorScheme values which we receive as props in the `Alert` component.

- We then create our `AlertContainer` styled component and create our variants. Finally we create our Alert Component and Wrap it inside the Provider.

### AlertTitle & AlertDescription Components

- Below the above code under `molecules/alert/index.tsx` paste the following code -

```tsx
export interface AlertTitleProps extends BoxProps {}

export const AlertTitle = React.forwardRef<HTMLDivElement, AlertTitleProps>(
  (props, ref) => {
    const { children, ...delegated } = props;

    return (
      <Box ref={ref} fontWeight="bold" lineHeight="tall" {...delegated}>
        {children}
      </Box>
    );
  }
);

export interface AlertDescriptionProps extends BoxProps {}

export const AlertDescription = React.forwardRef<
  HTMLDivElement,
  AlertDescriptionProps
>((props, ref) => {
  return <Box ref={ref} display="inline" lineHeight="taller" {...props} />;
});
```

### AlertIcon Component

- Here we will use our context values. As stated above our `AlertIcon` changes it colors depending on the variant passed to the `Alert` component.

- Below the above code under `molecules/alert/index.tsx` paste the following code -

```tsx
const alertIconVariants = (colorScheme: ColorScheme) => {
  const color = `${colorScheme}500`;

  return {
    subtle: {
      color,
    },
    solid: {
      color: "white",
    },
    "left-accent": {
      color,
    },
    "top-accent": {
      color,
    },
  };
};

const AlertIconContainer = styled(Box)<AlertProps>`
  display: inherit;

  ${({ colorScheme = "gray" }) =>
    variantFun({
      prop: "variant",
      variants: alertIconVariants(colorScheme),
    })}
`;

export interface AlertIconProps extends BoxProps {}

export const AlertIcon: React.FC<AlertIconProps> = (props) => {
  const { status, variant, colorScheme } = useAlertContext();
  const { colorScheme: statusColorScheme, icon: BaseIcon } = STATUSES[status];

  const iconColorScheme = colorScheme ?? statusColorScheme;

  return (
    <AlertIconContainer
      as="span"
      variant={variant}
      colorScheme={iconColorScheme}
      flexShrink="0"
      marginEnd="sm"
      width="1.25rem"
      height="1.5rem"
      {...props}
    >
      <BaseIcon width="100%" height="100%" />
    </AlertIconContainer>
  );
};
```

- We create our variants, pick the context values and pass them to the `AlertIconContainer`.

### Story

- With the above our `Alert` component is completed.
- Under the `src/components/molecules/alert/alert.stories.tsx` file we add the following for default story -

```tsx
import * as React from "react";

import { colorSchemeOptions } from "../../../theme/colors";
import { Box, Stack } from "../../atoms/layout";
import { CloseButton } from "../../atoms/form";
import { Alert, AlertIcon, AlertTitle, AlertDescription, AlertProps } from ".";

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

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

export const Default = {
  render: () => (
    <Stack direction="column" spacing="lg">
      <Stack direction="column" spacing="lg">
        <Alert>
          <AlertIcon />
          There was an error processing your request
        </Alert>

        <Alert status="error">
          <AlertIcon />
          <AlertTitle marginRight="md">Your browser is outdated!</AlertTitle>
          <AlertDescription>
            Your Chakra experience may be degraded.
          </AlertDescription>
          <CloseButton position="absolute" right="8px" top="15px" />
        </Alert>

        <Alert status="success">
          <AlertIcon />
          Data uploaded to the server. Fire on!
        </Alert>

        <Alert status="warning">
          <AlertIcon />
          Seems your account is about expire, upgrade now
        </Alert>

        <Alert status="info">
          <AlertIcon />
          Chakra is going live on August 30th. Get ready!
        </Alert>
      </Stack>
      <Stack direction="column" spacing="lg">
        <Alert status="success" variant="subtle">
          <AlertIcon />
          Data uploaded to the server. Fire on!
        </Alert>

        <Alert status="success" variant="solid">
          <AlertIcon />
          Data uploaded to the server. Fire on!
        </Alert>

        <Alert status="success" variant="left-accent">
          <AlertIcon />
          Data uploaded to the server. Fire on!
        </Alert>

        <Alert status="success" variant="top-accent">
          <AlertIcon />
          Data uploaded to the server. Fire on!
        </Alert>
      </Stack>
      <Stack>
        <Alert
          status="success"
          variant="subtle"
          textAlign="center"
          height="200px"
          direction="column"
          justify="center"
        >
          <AlertIcon size="40px" />
          <AlertTitle margin="md" fontSize="lg">
            Application submitted!
          </AlertTitle>
          <AlertDescription>
            Thanks for submitting your application. Our team will get back to
            you soon.
          </AlertDescription>
        </Alert>
      </Stack>
      <Stack>
        <Alert status="success">
          <AlertIcon />
          <Box flex="1">
            <AlertTitle>Success!</AlertTitle>
            <AlertDescription display="block">
              Your application has been received. We will review your
              application and respond within the next 48 hours.
            </AlertDescription>
          </Box>
          <CloseButton position="absolute" right="8px" top="8px" />
        </Alert>
      </Stack>
    </Stack>
  ),
};
```

### Build the Library

- Under the `molecules/index.ts` file paste the following -

```ts
export * from "./alert";
```

- Now `npm run build`.

- Under the folder `example/src/App.tsx` we can test our `Avatar` component. Copy paste the default story code and then run `npm run start` from the `example` directory.

### Summary

There you go guys in this tutorial we created `Avatar` component just like chakra ui. You can find the code for this tutorial under the **molecule-alert** branch [here](https://github.com/yaldram/chakra-ui-clone). And we are Done, thanks a lot. I do have some additional components like `Tag`, `Badge`, `Alert` and `Toast` if you followed this series up till here all these are self-explanatory check the repo for more. Please leave your valuable feedback and comments also share these tutorials with others. Until next time PEACE.