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)