ReactHustle

Create a React Confirm Alert Component From Scratch using TailwindCSS and HeadlessUI

Jasser Mark Arioste

Jasser Mark Arioste

Create a React Confirm Alert Component From Scratch using TailwindCSS and HeadlessUI

In this tutorial, you'll learn how to create a react confirm alert dialog component, step-by-step, from scratch with minimal dependencies, and only using TailwindCSS, HeadlessUI, and Typescript.

If you're working on an existing project or don't like to use TailwindCSS, It's completely fine. TailwindCSS is only used for styling and you can substitute any UI Framework you like such as MUI. I'll also show an example of how it's done using MUI.

Introduction #

Sometimes we need to create custom components to ensure a uniform display and behavior across each browser. And sometimes we just hate the browser's default confirm dialog. In this tutorial, we'll dissect each part of the confirm dialog component and implement in a maintainable way and use it intuitively using react custom hooks.

What is a Confirm Dialog? #

A confirm dialog is a dialog that asks the user for confirmation about a certain action. By default, you can invoke a confirm dialog in the browser API by using window.confirm function. A screenshot can be seen below:

Default Confirm Dialog of Chrome Browser

Tutorial Objectives #

In this tutorial we have the following objectives:

  1. Create a confirm dialog component that can be used throughout the whole app.
  2. We should be able to customize the dialog component, title, message, button texts, etc.
  3. It should work on asynchronous operations. For example, if we click OK and we call a fetch request somewhere, it should show a loading state.
  4. It should be easy to use. We'll do this by creating a custom hook.
  5. Use HeadlessUI and Tailwind CSS to style the Confirm Dialog.

Final Output #

Here's the final output of what we'll be making:

React Confirm Alert Final output

Project Setup #

We'll be using NextJS as our default framework for this project. If you want to follow along, I created a NextJS starter template on GitHub that already includes TailwindCSS. You can initialize immediately by running the command:

npx create-next-app -e https://github.com/jmarioste/next-tailwind-starter-2 react-confirm-alert-tutorial

If you're using a different framework (e.g., CRA, Vite) in an existing project, be sure to check tailwind docs on how to install tailwind for your specific framework.

All right, the next step is implementation. We're going to do it in reverse this time. We're going to think about the usage rather than building the react component first. How do we invoke the confirm dialog? What API makes sense? And from there, we're going to implement things step-by-step.

Step 1 - How to Invoke the Confirm Dialog #

When creating components like modals or dialogs, I think it's best to start thinking about the usage first rather than building the component. In my experience in building components in react, and using different libraries in general, I think one good way to do this is to use a custom hook. For example:

// pages/index.tsx
import type { NextPage } from "next";
// import hook, take note of the file as we'll create it in the next step
import { useConfirmAlert } from "../components/alert/AlertProvider";
const Home: NextPage = () => {
  const { showAlert } = useConfirmAlert();
  const handleClick = () => {
    showAlert({
      title: "Confirm Deletion",
      confirmMessage: "Are you sure you want to delete this?",
      async onConfirm() {
        console.log("Stuff has been deleted");
      },
    });
  };
  return (
    <div className="container mx-auto">
      <button onClick={handleClick}>Show alert</button>
    </div>
  );
};
export default Home;
12345678910111213141516171819202122

Explanation:

In line 6, we use a custom hook useConfirmAlert that returns a showAlert function. The showAlert function is us trying to mimic the window.confirm function.

In lines 11-13, we attach a onCofirm callback for when the user clicks "OK". The callback function can be an asynchronous function to take into account stuff like calling API calls, which is a very common scenario.  

We can also modify the title and confirmMessage whenever we show the confirm dialog. For starters, I think this is a pretty good API.

For now, this will result in a compile-time error since we haven't defined the useConfirmAlert hook. The next step is to create the React.Context and define the custom hook.

Step 2 - Creating the Context and CustomHook #

The next step is to create the custom hook and react context so that the previous step won't have a compile-time error. So create the file components/alert/AlertProvider.tsx and copy the code below:

// components/alert/AlertProvider.tsx
// 👇 options for showAlert function
type AlertOptions = {
  title: ReactNode;
  confirmMessage: ReactNode;
  onConfirm(): Promise<void> | void;
};
// 👇 define the AlertContext
const AlertContext = createContext<{
  showAlert(opts: AlertOptions): void;
} | null>(null);
const AlertProvider = () => {
  //we will implement this in the next step
  return null;
}
export default AlertProvider
// 👇 define the useConfirmAlert hook
export const useConfirmAlert = () => {
  const context = useContext(AlertContext);
  if (!context) {
    throw new Error("Please Use AlertProvider in parent component.");
  }
  return context;
};
123456789101112131415161718192021222324

Now, that we've defined the hook. There's no more compile-time error but we now have a run-time error since we don't have the AlertContext.Provider in the parent component:

Custom hook error in React

If we want to use React.Context, we have to have the Provider in the somewhere parent element. In the next step, we'll create the AlertProvider to fix this error.

Step 3 - Creating the AlertProvider component. #

The next step is to create the AlertProvider component, but let's also think about its usage. Since we want to be able to customize the alert dialog, I would like to use it like this:

// pages/_app.tsx
import "../styles/globals.css";
import type { AppProps } from "next/app";
import AlertProvider from "../components/alert/AlertProvider";
import AlertDialog from "../components/alert/AlertDialog";
function MyApp({ Component, pageProps }: AppProps) {
  return (
    // 👇 we should be able to specficy the AlertDialog we want to use
    // whether it's implemented using @headlessui + tailwind or MUI or bootstrap
    <AlertProvider AlertComponent={AlertDialog}>
      <Component {...pageProps} />
    </AlertProvider>
  );
}
export default MyApp;
123456789101112131415

Copy the code above to pages/_app.tsx and we'll define the AlertProvider next to fix the compile-time error.

Next, in components/alert/AlertProvider.tsx, add the highlighted code below:

// components/alert/AlertProvider.tsx
import React, {
  createContext,
  PropsWithChildren,
  ReactNode,
  useContext,
  useState,
} from "react";

type AlertOptions = {
   // ...omitted for brevity
};
const AlertContext = createContext<{
  showAlert(opts: AlertOptions): void;
} | null>(null);

/**
 * Any AlertDialog component used with AlertProvider should use these props
 */
export type AlertComponentProps = {
  open: boolean;
  message: ReactNode;
  title: ReactNode;
  onClose(): void;
  onConfirm(): Promise<void> | void;
  confirming?: boolean;
};

/**
 * Props for AlertProvider. 
 * AlertComponent is a React.ComponentType with AlertComponentProps. 
 * This is for type safety, if you pass a different component it will result in an error.
 */
export type AlertProviderProps = {
  AlertComponent: React.ComponentType<AlertComponentProps>;
} & PropsWithChildren;

/**
 * Alert provider definition 
 */
const AlertProvider = ({ AlertComponent, children }: AlertProviderProps) => {
  const [shown, setShown] = useState(false);
  const [loading, setLoading] = useState(false);
  const defaultOptions: AlertOptions = {
    title: "Confirm",
    confirmMessage: "Are you sure?",
    async onConfirm() {
      setShown(false);
    },
  };
  const [alertOptions, setAlertOptions] =
    useState<AlertOptions>(defaultOptions);

  const showAlert = (opts?: Partial<AlertOptions>) => {
    setShown(true);
    setAlertOptions({
      confirmMessage: opts?.confirmMessage ?? defaultOptions.confirmMessage,
      onConfirm: opts?.onConfirm ?? defaultOptions.onConfirm,
      title: opts?.title ?? defaultOptions.title,
    });
  };

  const hideAlert = () => setShown(false);
  const onConfirm = async () => {
    setLoading(true);
    alertOptions.onConfirm && (await alertOptions.onConfirm());
    setLoading(false);
    setShown(false);
  };
  return (
    <AlertContext.Provider value={{ showAlert }}>
      <AlertComponent
        open={shown}
        onClose={hideAlert}
        onConfirm={onConfirm}
        message={alertOptions.confirmMessage}
        title={alertOptions.title}
        confirming={loading}
      />
      {children}
    </AlertContext.Provider>
  );
};
export default AlertProvider;
export const useConfirmAlert = () => {
  // ...omitted for brevity
};
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687

Explanation:

Let's skip ahead to lines 65-75 because this is where the magic happens.

In line 65: we use the AlertContextProvider and pass the showAlert function. The showAlert function modifies the state so that the title, message and onConfirm properties are using the ones from the latest call.

Lines 66-73: Once the state changes, it re-renders the AlertComponent showing the latest state.

Line 58-63: the onConfirm function sets the loading to true, and "awaits" for the latest alertOptions.onConfirm to finish. Once it finishes, it sets the loading state to false and hides the dialog.

Here, we don't know how the AlertComponent is implemented, we just know that it has these AlertComponentProps that we can control. The next step is to implement the AlertDialog component.

Step 4 - Creating the AlertDialog Component #

Let's implement the AlertDialog component. First, lets install the dependency, @headlessui/react by running the command:

yarn add @headlessui/react

Next, create the file components/alert/AlertDialog.tsx. In HeadlessUI Docs, they already have a Dialog component that we can copy from and modify to fit our needs. The code below shows a simple dialog without any transition animations:

// components/alert/AlertDialog.tsx
import { Dialog } from "@headlessui/react";
import React from "react";
import { AlertComponentProps } from "./AlertProvider";
const AlertDialog = (props: AlertComponentProps) => {
  return (
    <Dialog
      as="div"
      className="relative z-10"
      onClose={props.onClose}
      open={props.open}
    >
      <div className="fixed inset-0 overflow-y-auto">
        <div className="flex min-h-full items-center justify-center p-4 text-center">
          <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
            <Dialog.Title
              as="h3"
              className="text-lg font-medium leading-6 text-gray-900"
            >
              {props.title}
            </Dialog.Title>
            <div className="mt-2">
              <p className="text-sm text-gray-500">{props.message}</p>
            </div>
            <div className="flex gap-2 mt-4">
              <button
                type="button"
                className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
                onClick={props.onConfirm}
              >
                {props.confirming ? "Loading..." : "Yes"}
              </button>
              <button
                type="button"
                className="inline-flex justify-center rounded-md border border-transparent bg-red-100 px-4 py-2 text-sm font-medium text-red-900 hover:bg-red-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
                onClick={props.onClose}
              >
                No
              </button>
            </div>
          </Dialog.Panel>
        </div>
      </div>
    </Dialog>
  );
};
export default AlertDialog;
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

Explanation:

In lines 4-5: We use the AlertComponentProps defined by AlertProvider.

Lines 27 & 29: It uses the props.onConfirm function when the "Yes" button is clicked. and shows a loading text when it's still in "confirming" state. 

Step 5 - Usage #

To use the AlertDialog, we just pass it into our AlertProvider in pages/_app.tsx:

import "../styles/globals.css";
import type { AppProps } from "next/app";
import AlertProvider from "../components/alert/AlertProvider";
import AlertDialog from "../components/alert/AlertDialog";
function MyApp({ Component, pageProps }: AppProps) {
  return (
    // 👇 we should be able to specficy the AlertDialog we want to use
    // whether it's implemented using @headlessui + tailwind or MUI or bootstrap
    <AlertProvider AlertComponent={AlertDialog}>
      <Component {...pageProps} />
    </AlertProvider>
  );
}
export default MyApp;
1234567891011121314

Full Code and Demo #

The full code can be accessed at GitHub: React Confirm Alert Tutorial

The demo can be accessed at StackBlitz: React Confirm Alert Tutorial

Bonus Step - Creating an AlertDialog using MUI #

If you're using MUI, you can create the dialog by using the following code:    

// components/alert/MuiAlertDialog.tsx
import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
} from "@mui/material";
import { AlertComponentProps } from "./AlertProvider";
const MuiAlertDialog = (props: AlertComponentProps) => {
  return (
    <Dialog
      open={props.open}
      onClose={props.onClose}
      aria-labelledby="alert-dialog-title"
      aria-describedby="alert-dialog-description"
    >
      <DialogTitle id="alert-dialog-title">{props.title}</DialogTitle>
      <DialogContent>
        <DialogContentText id="alert-dialog-description">
          {props.message}
        </DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button onClick={props.onClose}>No</Button>
        <Button onClick={props.onConfirm} autoFocus>
          {props.confirming ? "Loading..." : "Yes"}
        </Button>
      </DialogActions>
    </Dialog>
  );
};
export default MuiAlertDialog;
12345678910111213141516171819202122232425262728293031323334

Usage in _app.tsx is similar to the previous one:

// pages/_app.tsx
import "../styles/globals.css";
...
import MuiAlertDialog from "../components/alert/MuiAlertDialog";
function MyApp({ Component, pageProps }: AppProps) {
  return (
    // 👇 we should be able to specficy the AlertDialog we want to use
    // whether it's implemented using @headlessui + tailwind or MUI or bootstrap
    <AlertProvider AlertComponent={MuiAlertDialog}>
      <Component {...pageProps} />
    </AlertProvider>
  );
}
export default MyApp;
1234567891011121314

Conclusion #

We learned how to create a pretty good react confirm alert dialog by using react hooks, Tailwindcss and HeadlessUI. I think the reverse method discussed in this tutorial is extremely important not just in being a front-end developer, but as a programmer in general when implementing anything related to programming. 

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on Twitter.

Credits: Image by Erich Westendarp from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle