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.

shell
npm install overlay-kit @headlessui/react

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