Download as pdf or txt
Download as pdf or txt
You are on page 1of 12

Dialog (Modal)

A fully-managed, renderless dialog component jam-packed with accessibility and keyboard


features, perfect for building completely custom modal and dialog windows for your next
application.

Preview Code Copy

Installation

To get started, install Headless UI via npm:

npm install @headlessui/react

Basic example

Dialogs are built using the Dialog, Dialog.Panel, Dialog.Title and Dialog.Description components.

When the dialog's open prop is true, the contents of the dialog will render. Focus will be moved inside the
dialog and trapped there as the user cycles through the focusable elements. Scroll is locked, the rest of
your application UI is hidden from screen readers, and clicking outside the Dialog.Panel or pressing the
Escape key will fire the close event and close the dialog.

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>
)
}

If your dialog has a title and description, use the Dialog.Title and Dialog.Description components to
provide the most accessible experience. This will link your title and description to the root dialog
component via the aria-labelledby and aria-describedby attributes, ensuring their contents are
announced to users using screenreaders when your dialog opens.

Showing and hiding your dialog

Dialogs have no automatic management of their open/closed state. To show and hide your dialog, pass
React state into the open prop. When open is true the dialog will render, and when it's false the dialog will
unmount.

The onClose callback fires when an open dialog is dismissed, which happens when the user clicks outside
the your Dialog.Panel or presses the Escape key. You can use this callback to set open back to false and
close your dialog.

import { useState } from 'react'


import { Dialog } from '@headlessui/react'

function MyDialog() {
// The open/closed state lives outside of the Dialog and is managed by you
let [isOpen, setIsOpen] = useState(true)

function handleDeactivate() {
// ...
}

return (
/*
Pass `isOpen` to the `open` prop, and use `onClose` to set
the state back to `false` when the user clicks outside of
the dialog or presses the escape key.
*/
<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>

{/*
You can render additional buttons to dismiss your dialog by setting
`isOpen` to `false`.
*/}
<button onClick={() => setIsOpen(false)}>Cancel</button>
<button onClick={handleDeactivate}>Deactivate</button>
</Dialog.Panel>
</Dialog>
)
}

Styling the dialog

Style the Dialog and Dialog.Panel components using the className or style props like you would with any
other element. You can also introduce additional elements if needed to achieve a particular design.

import { useState } from 'react'


import { Dialog } from '@headlessui/react'

function MyDialog() {
let [isOpen, setIsOpen] = useState(true)

return (
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="relative z-50"
>
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="w-full max-w-sm rounded bg-white">
<Dialog.Title>Complete your order</Dialog.Title>

{/* ... */}


</Dialog.Panel>
</div>
</Dialog>
)
}
Clicking outside the Dialog.Panel component will close the dialog, so keep that in mind when deciding
which styles to apply to which elements.

Adding a backdrop

If you'd like to add an overlay or backdrop behind your Dialog.Panel to bring attention to the panel itself,
we recommend using a dedicated element just for the backdrop and making it a sibling to your panel
container:

import { useState } from 'react'


import { Dialog } from '@headlessui/react'

function MyDialog() {
let [isOpen, setIsOpen] = useState(true)

return (
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="relative z-50"
>
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />

{/* Full-screen container to center the panel */}


<div className="fixed inset-0 flex items-center justify-center p-4">
{/* The actual dialog panel */}
<Dialog.Panel className="mx-auto max-w-sm rounded bg-white">
<Dialog.Title>Complete your order</Dialog.Title>

{/* ... */}


</Dialog.Panel>
</div>
</Dialog>
)
}

This lets you transition the backdrop and panel independently with their own animations, and rendering it
as a sibling ensures that it doesn't interfere with your ability to scroll long dialogs.

Scrollable dialogs

Making a dialog scrollable is handled entirely in CSS, and the specific implementation depends on the
design you are trying to achieve.

Here's an example where the entire panel container is scrollable, and the panel itself moves as you scroll:
import { useState } from 'react'
import { Dialog } from '@headlessui/react'

function MyDialog() {
let [isOpen, setIsOpen] = useState(true)

return (
<Dialog
open={isOpen}
onClose={() => setIsOpen(false)}
className="relative z-50"
>
{/* The backdrop, rendered as a fixed sibling to the panel container */}
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />

{/* Full-screen scrollable container */}


<div className="fixed inset-0 overflow-y-auto">
{/* Container to center the panel */}
<div className="flex min-h-full items-center justify-center p-4">
{/* The actual dialog panel */}
<Dialog.Panel className="mx-auto max-w-sm rounded bg-white">
<Dialog.Title>Complete your order</Dialog.Title>

{/* ... */}


</Dialog.Panel>
</div>
</div>
</Dialog>
)
}

When creating a scrollable dialog with a backdrop, make sure the backdrop is rendered behind the
scrollable container, otherwise the scroll wheel won't work when hovering over the backdrop, and the
backdrop may obscure the scrollbar and prevent users from clicking it with their mouse.

Managing initial focus

For accessibility reasons, your dialog should contain at least one focusable element. By default,
the Dialog component will focus the first focusable element (by DOM order) once it is rendered, and
pressing the Tab key will cycle through all additional focusable elements within the contents.

Focus is trapped within the dialog as long as it is rendered, so tabbing to the end will start cycling back
through the beginning again. All other application elements outside of the dialog will be marked as inert
and thus not focusable.

If you'd like something other than the first focusable element to receive initial focus when your dialog is
initially rendered, you can use the initialFocus ref:
import { useState, useRef } from 'react'
import { Dialog } from '@headlessui/react'

function MyDialog() {
let [isOpen, setIsOpen] = useState(true)
let completeButtonRef = useRef(null)

function completeOrder() {
// ...
}

return (
/* Use `initialFocus` to force initial focus to a specific ref. */
<Dialog
initialFocus={completeButtonRef}
open={isOpen}
onClose={() => setIsOpen(false)}
>
<Dialog.Panel>
<Dialog.Title>Complete your order</Dialog.Title>

<p>Your order is all ready!</p>

<button onClick={() => setIsOpen(false)}>Cancel</button>


<button ref={completeButtonRef} onClick={completeOrder}>
Complete order
</button>
</Dialog.Panel>
</Dialog>
)
}

Rendering to a portal

If you've ever implemented a Dialog before, you've probably come across Portals in React. Portals let you
invoke components from one place in the DOM (for instance deep within your application UI), but actually
render to another place in the DOM entirely.

Since Dialogs and their backdrops take up the full page, you typically want to render them as a sibling to
the root-most node of your React application. That way you can rely on natural DOM ordering to ensure
that their content is rendered on top of your existing application UI. This also makes it easy to apply scroll
locking to the rest of your application, as well as ensure that your Dialog's contents and backdrop are
unobstructed to receive focus and click events.

Because of these accessibility concerns, Headless UI's Dialog component actually uses a Portal under-the-
hood. This way we can provide features like unobstructed event handling and making the rest of your
application inert. So, when using our Dialog, there's no need to use a Portal yourself! We've already taken
care of it.
Transitions

To animate the opening/closing of the dialog, use the Transition component. All you need to do is wrap
the Dialog in a <Transition>, and dialog will transition automatically based on the state of the show prop on
the <Transition>.

When using <Transition> with your dialogs, you can remove the open prop, as the dialog will read
the show state from the <Transition> automatically.

import { useState, Fragment } from 'react'


import { Dialog, Transition } from '@headlessui/react'

function MyDialog() {
let [isOpen, setIsOpen] = useState(true)

return (
<Transition
show={isOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
as={Fragment}
>
<Dialog onClose={() => setIsOpen(false)}>
<Dialog.Panel>
<Dialog.Title>Deactivate account</Dialog.Title>
{/* ... */}
</Dialog.Panel>
</Dialog>
</Transition>
)
}

To animate your backdrop and panel separately, wrap your Dialog in Transition and wrap your backdrop
and panel each with their own Transition.Child:

import { useState, Fragment } from 'react'


import { Dialog, Transition } from '@headlessui/react'

function MyDialog() {
let [isOpen, setIsOpen] = useState(true)

return (
// Use the `Transition` component at the root level
<Transition show={isOpen} as={Fragment}>
<Dialog onClose={() => setIsOpen(false)}>
{/*
Use one Transition.Child to apply one transition to the backdrop...
*/}
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/30" />
</Transition.Child>

{/*
...and another Transition.Child to apply a separate transition
to the contents.
*/}
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel>
<Dialog.Title>Deactivate account</Dialog.Title>

{/* ... */}


</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
)
}

If you want to animate your dialogs using another animation library like Framer Motion or React Spring and
need more control, you can use the static prop to tell Headless UI not to manage rendering itself, and
control it manually with another tool:

import { useState } from 'react'


import { Dialog } from '@headlessui/react'
import { AnimatePresence, motion } from 'framer-motion'

function MyDialog() {
let [isOpen, setIsOpen] = useState(true)

return (
// Use the `Transition` component + show prop to add transitions.
<AnimatePresence>
{open && (
<Dialog
static
as={motion.div}
open={isOpen}
onClose={() => setIsOpen(false)}
>
<div className="fixed inset-0 bg-black/30" />

<Dialog.Panel>
<Dialog.Title>Deactivate account</Dialog.Title>

{/* ... */}


<Dialog.Panel>
</Dialog>
)}
</AnimatePresence>
)
}

The open prop is still used for manage scroll locking and focus trapping, but as long as static is present,
the actual element will always be rendered regardless of the open value, which allows you to control it
yourself externally.

Accessibility notes

Focus management
When the Dialog's open prop is true, the contents of the Dialog will render and focus will be moved inside
the Dialog and trapped there. The first focusable element according to DOM order will receive focus,
although you can use the initialFocus ref to control which element receives initial focus. Pressing Tab on
an open Dialog cycles through all the focusable elements.

Mouse interaction
When a Dialog is rendered, clicking outside of the Dialog.Panel will close the Dialog.

No mouse interaction to open the Dialog is included out-of-the-box, though typically you will wire a <button
/> element up with an onClick handler that toggles the Dialog's open prop to true.

Keyboard interaction

Command Description

Esc Closes any open Dialogs


Tab Cycles through an open Dialog's contents

Shift + Tab Cycles backwards through an open Dialog's contents

Other
When a Dialog is open, scroll is locked and the rest of your application UI is hidden from screen readers.

All relevant ARIA attributes are automatically managed.

Component API

Dialog
The main Dialog component.

Prop Default Description

open — Boolean

Whether the Dialog is open or not.

onClose — (false) => void

Called when the Dialog is dismissed (via outside click of the Dialog.Panel or
by pressing the Escape key). Typically used to close the dialog by
setting open to false.

initialFocus — React.MutableRefObject

A ref to an element that should receive focus first.

as div String | Component

The element or component the Dialog should render as.

static false Boolean

Whether the element should ignore the internally managed open/closed


state.

unmount true Boolean

Whether the element should be unmounted or hidden based on the


open/closed state.
Render Prop Description

open Boolean

Whether or not the dialog is open.

Dialog.Panel
This indicates the panel of your actual Dialog. Clicking outside of this component will trigger the onClose of
the Dialog component.

Prop Default Description

as div String | Component

The element or component the Dialog.Panel should render as.

Render Prop Description

open Boolean

Whether or not the dialog is open.

Dialog.Title
This is the title for your Dialog. When this is used, it will set the aria-labelledby on the Dialog.

Prop Default Description

as h2 String | Component

The element or component the Dialog.Title should render as.

Render Prop Description

open Boolean

Whether or not the dialog is open.

Dialog.Description
This is the description for your Dialog. When this is used, it will set the aria-describedby on the Dialog.
Prop Default Description

as p String | Component

The element or component the Dialog.Description should render as.

Render Prop Description

open Boolean

Whether or not the dialog is open.

Dialog.Overlay
As of Headless UI v1.6, Dialog.Overlay is deprecated, see the release notes for migration instructions.

Prop Default Description

as div String | Component

The element or component the Dialog.Overlay should render as.

Render Prop Description

open Boolean

Whether or not the dialog is open.

Styled examples

If you're interested in predesigned component examples using Headless UI and Tailwind CSS, check
out Tailwind UI — a collection of beautifully designed and expertly crafted components built by us.

It's a great way to support our work on open-source projects like this and makes it possible for us to
improve them and keep them well-maintained.

You might also like