Converting Our Modal
Video Summary
In this lesson, we migrate an application that uses our home-grown Modal
component to use the Dialog
component from the Headless UI library.
This application includes a Header
component that consumes the modal, like so:
function Header() { const [isModalOpen, toggleIsModalOpen] = useToggle(false);
return ( <header> {isModalOpen && ( <Modal title="Log in" handleDismiss={() => toggleIsModalOpen(false)} > <LoginForm /> </Modal> )} <button onClick={toggleIsModalOpen}> Log in </button> </header> );}
This sandbox comes with the Modal
component we built in a previous lesson. Our goal is to update this component to use the Dialog
component from Headless UI.
We can grab an example implementation from the docs, to use as a reference:
import { useState } from 'react'import { Dialog } from '@headlessui/react'
function MyDialog() { let [isOpen, setIsOpen] = useState(true)
return ( <Dialog open={isOpen} onClose={() => setIsOpen(false)}> <Dialog.Panel> <Dialog.Title>Deactivate account</Dialog.Title> <Dialog.Description> This will permanently deactivate your account </Dialog.Description>
<p> Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone. </p>
<button onClick={() => setIsOpen(false)}>Deactivate</button> <button onClick={() => setIsOpen(false)}>Cancel</button> </Dialog.Panel> </Dialog> )}
Dialog
is the wrapper component, Dialog.Panel
is the box itself that includes content. Dialog.Title
is for the modal's title, and Dialog.Description
holds an optional description. These components use the Compound Components pattern we learned about earlier.
The Dialog
component accepts an open
prop, and this is something our Modal
doesn't currently support. We've been using conditional rendering to show/hide the modal, but it's common for third-party Modal components to have a prop that controls this.
We could solve this by locking the open
prop to true
. If these elements are being rendered, we know the modal must be open! But I feel like this just looks strange / requires too much explanation:
<Dialog open={true}>
Instead, we'll add an isOpen
prop to our Modal
component.
Inside Modal
, we'll use these various Dialog elements, fitting them in with our component. We're left with this:
function Modal({ title, description, isOpen, handleDismiss, children,}) { return ( <Dialog open={isOpen} onClose={handleDismiss}> <Dialog.Panel> <Dialog.Title>{title}</Dialog.Title> {description && ( <Dialog.Description> {description} </Dialog.Description> )} {children} </Dialog.Panel> </Dialog> );}
If we test this out, we'll see that clicking the "Log in" button does add a login form to the page, but it doesn't appear to be in a modal. It's not floating above the content, it's sitting underneath!
This is why Headless UI is considered an “unstyled” component library. It doesn't come with any CSS, out of the box! Not even critical styles, like setting position: fixed
on the modal to float it above the content.
We can fix this by adding the CSS classes we were already using:
function Modal({ title, description, isOpen, handleDismiss, children,}) { return ( <Dialog className={styles.wrapper} open={isOpen} onClose={handleDismiss} > <div className={styles.backdrop} onClick={handleDismiss} /> <Dialog.Panel className={styles.dialog}> <Dialog.Title>{title}</Dialog.Title> {description && ( <Dialog.Description> {description} </Dialog.Description> )} {children} </Dialog.Panel> </Dialog> );}
We've added classes to Dialog
and Dialog.Panel
. Also, we had to bring over the “backdrop” element, since HeadlessUI's dialog doesn't come with one by default.
Finally, we need to add our close button:
<button className={styles.closeBtn} onClick={handleDismiss}> <Close /> <VisuallyHidden>Dismiss modal</VisuallyHidden></button>
With that, our modal migration is complete!
Why do we still have a Modal component? Can't we use the Dialog
that we get from Headless UI directly, whenever we need a modal (eg. in Header
)?
Well, we can, but it would be an awful lot of work. We've customized the modal quite a bit, with our custom styles, backdrop, and close button.
There are two more reasons that it's worthwhile to have our own wrappers around third-party library components:
- We can choose a consistent prop API. In a larger app, we might use components from 7 different libraries and packages, and they all have their own names for common props like
isOpen
andhandleDismiss
. If we create our own wrappers, we have total control over the interface, and we can make sure it's consistent. - We might decide, later, to migrate to a different third-party component. This way, we only have to change 1 file. We don't have to make a bunch of changes to every single component that has a modal.
Here's the original code from the video, in case you wanted to practice migrating the modal:
Code Playground
…and here's the converted code, using the Headless UI library:
Code Playground