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
Under the
components/molecules
folder create a new folder calledalert
. 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 theAlertIcon
. 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 calledcontext.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>;
}
-
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>
);
}
);
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} />;
});
AlertIcon Component
Here we will use our context values. As stated above our
AlertIcon
changes it colors depending on the variant passed to theAlert
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>
);
};
- 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>
),
};
Build the Library
- Under the
molecules/index.ts
file paste the following -
export * from "./alert";
Now
npm run build
.Under the folder
example/src/App.tsx
we can test ourAvatar
component. Copy paste the default story code and then runnpm run start
from theexample
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.
Top comments (0)