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. All the code for this series is available on GitHub.
Step One: Alert styles & theming
We want to achieve the following for the Alert
component -
<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 -
<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 -
@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 ofcolorScheme
andvariants
, 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 acss 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
-
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 -
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 -
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. In the next tutorial we will work on the Button
component. Until next time PEACE.
Top comments (0)