With Headless UI
Learn how to use Headless UI’s Dialog component together with overlay-kit.
Installation
Since Headless UI ships unstyled headless primitives, the examples below pair it with Tailwind CSS.
npm install overlay-kit @headlessui/reactBasic Usage
Headless UI Dialog takes an open prop for visibility and onClose for dismissal. onClose is invoked automatically on backdrop clicks and Escape key presses, so you can pass close directly.
import { OverlayProvider, overlay } from 'overlay-kit'; import { Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description } from '@headlessui/react'; function App() { return ( <button className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700" onClick={() => { overlay.open(({ isOpen, close }) => ( <Dialog open={isOpen} onClose={close} className="relative z-50"> <DialogBackdrop className="fixed inset-0 bg-black/50" /> <div className="fixed inset-0 flex items-center justify-center p-4"> <DialogPanel className="w-full max-w-md space-y-4 rounded-lg bg-white p-6 shadow-xl"> <DialogTitle className="text-lg font-semibold">Are you sure you want to continue?</DialogTitle> <Description className="text-sm text-gray-500">This action cannot be undone.</Description> <div className="flex justify-end gap-2"> <button className="rounded-md border border-gray-200 px-4 py-2 text-sm font-medium hover:bg-gray-100" onClick={close} > No </button> <button className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700" onClick={close} > Yes </button> </div> </DialogPanel> </div> </Dialog> )); }} > Open Confirm Dialog </button> ); } export function Example() { return ( <OverlayProvider> <App /> </OverlayProvider> ); }
Receiving Async Results
Use overlay.openAsync to receive the user’s choice as a Promise. Pass the result through close(value) on each button, and provide a default value in onClose for backdrop or Escape dismissal.
import { useState } from 'react'; import { OverlayProvider, overlay } from 'overlay-kit'; import { Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description } from '@headlessui/react'; function App() { const [result, setResult] = useState<boolean | null>(null); return ( <div> <p className="mb-2 text-sm">Result: {result === null ? 'Not selected' : result ? 'Yes' : 'No'}</p> <button className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700" onClick={async () => { const confirmed = await overlay.openAsync<boolean>(({ isOpen, close }) => ( <Dialog open={isOpen} onClose={() => close(false)} className="relative z-50"> <DialogBackdrop className="fixed inset-0 bg-black/50" /> <div className="fixed inset-0 flex items-center justify-center p-4"> <DialogPanel className="w-full max-w-md space-y-4 rounded-lg bg-white p-6 shadow-xl"> <DialogTitle className="text-lg font-semibold">Are you sure you want to continue?</DialogTitle> <Description className="text-sm text-gray-500">This action cannot be undone.</Description> <div className="flex justify-end gap-2"> <button className="rounded-md border border-gray-200 px-4 py-2 text-sm font-medium hover:bg-gray-100" onClick={() => close(false)} > No </button> <button className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700" onClick={() => close(true)} > Yes </button> </div> </DialogPanel> </div> </Dialog> )); setResult(confirmed); }} > Open Confirm Dialog </button> </div> ); } export function Example() { return ( <OverlayProvider> <App /> </OverlayProvider> ); }
Releasing Memory After Animation
Headless UI Dialog does not handle transitions on its own and is typically wrapped with <Transition>. Passing unmount to the <Transition>’s afterLeave callback releases overlay memory safely right after the close animation finishes.
import { Fragment } from 'react'; import { OverlayProvider, overlay } from 'overlay-kit'; import { Dialog, DialogBackdrop, DialogPanel, DialogTitle, Description, Transition, TransitionChild, } from '@headlessui/react'; function App() { return ( <button className="inline-flex items-center justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700" onClick={() => { overlay.open(({ isOpen, close, unmount }) => ( <Transition show={isOpen} as={Fragment} afterLeave={unmount}> <Dialog onClose={close} className="relative z-50"> <TransitionChild as={Fragment} enter="ease-out duration-200" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-150" leaveFrom="opacity-100" leaveTo="opacity-0" > <DialogBackdrop className="fixed inset-0 bg-black/50" /> </TransitionChild> <div className="fixed inset-0 flex items-center justify-center p-4"> <TransitionChild as={Fragment} enter="ease-out duration-200" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-150" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > <DialogPanel className="w-full max-w-md space-y-4 rounded-lg bg-white p-6 shadow-xl"> <DialogTitle className="text-lg font-semibold">Are you sure you want to continue?</DialogTitle> <Description className="text-sm text-gray-500">This action cannot be undone.</Description> <div className="flex justify-end gap-2"> <button className="rounded-md border border-gray-200 px-4 py-2 text-sm font-medium hover:bg-gray-100" onClick={close} > No </button> <button className="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700" onClick={close} > Yes </button> </div> </DialogPanel> </TransitionChild> </div> </Dialog> </Transition> )); }} > Open Confirm Dialog </button> ); } export function Example() { return ( <OverlayProvider> <App /> </OverlayProvider> ); }