Create a React Confirm Alert Component From Scratch using TailwindCSS and HeadlessUI
Jasser Mark Arioste
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:
Tutorial Objectives #
In this tutorial we have the following objectives:
- Create a confirm dialog component that can be used throughout the whole app.
- We should be able to customize the dialog component, title, message, button texts, etc.
- 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.
- It should be easy to use. We'll do this by creating a custom hook.
- Use HeadlessUI and Tailwind CSS to style the Confirm Dialog.
Final Output #
Here's the final output of what we'll be making:
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:
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