DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Build Chakra UI Alert component using react, typescript, styled-components and styled-system

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.
  • All the code for this tutorial can be found under the molecule-alert branch here.

Prerequisite

Please check the Chakra Alert Component code here. The theme / styles are here 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 -
git checkout -b molecule-alert
Enter fullscreen mode Exit fullscreen mode
  • 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 -

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

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 -

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>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • 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 -
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>
  ),
};
Enter fullscreen mode Exit fullscreen mode

Build the Library

  • Under the molecules/index.ts file paste the following -
export * from "./alert";
Enter fullscreen mode Exit fullscreen mode
  • 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. 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.

Discussion (0)