With Radix UI
Learn how to use Radix UI’s Dialog primitives together with overlay-kit.
Installation
Install the Radix Dialog package. Since Radix UI ships unstyled headless primitives, the examples below pair it with Tailwind CSS.
npm install overlay-kit @radix-ui/react-dialogBasic Usage
Radix Dialog.Root takes an open prop for visibility and onOpenChange for dismissal. onOpenChange receives a boolean, so calling close() when !open covers backdrop clicks and the Escape key naturally.
import { OverlayProvider, overlay } from 'overlay-kit'; import * as Dialog from '@radix-ui/react-dialog'; function App() { return ( <button className="inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" onClick={() => { overlay.open(({ isOpen, close }) => ( <Dialog.Root open={isOpen} onOpenChange={(open) => !open && close()}> <Dialog.Portal> <Dialog.Overlay className="fixed inset-0 bg-black/50" /> <Dialog.Content className="fixed left-1/2 top-1/2 z-50 grid w-full max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg bg-white p-6 shadow-lg"> <Dialog.Title className="text-lg font-semibold">Are you sure you want to continue?</Dialog.Title> <Dialog.Description className="text-sm text-gray-500">This action cannot be undone.</Dialog.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-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" onClick={close} > Yes </button> </div> </Dialog.Content> </Dialog.Portal> </Dialog.Root> )); }} > 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 when the dialog is dismissed via backdrop or Escape.
import { useState } from 'react'; import { OverlayProvider, overlay } from 'overlay-kit'; import * as Dialog from '@radix-ui/react-dialog'; 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-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" onClick={async () => { const confirmed = await overlay.openAsync<boolean>(({ isOpen, close }) => ( <Dialog.Root open={isOpen} onOpenChange={(open) => !open && close(false)}> <Dialog.Portal> <Dialog.Overlay className="fixed inset-0 bg-black/50" /> <Dialog.Content className="fixed left-1/2 top-1/2 z-50 grid w-full max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg bg-white p-6 shadow-lg"> <Dialog.Title className="text-lg font-semibold">Are you sure you want to continue?</Dialog.Title> <Dialog.Description className="text-sm text-gray-500"> This action cannot be undone. </Dialog.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-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" onClick={() => close(true)} > Yes </button> </div> </Dialog.Content> </Dialog.Portal> </Dialog.Root> )); setResult(confirmed); }} > Open Confirm Dialog </button> </div> ); } export function Example() { return ( <OverlayProvider> <App /> </OverlayProvider> ); }
Releasing Memory After Animation
Radix Dialog does not expose a dedicated animation-complete callback. Call close in onOpenChange and schedule unmount with setTimeout for the animation duration. This handles button clicks, backdrop clicks, and the Escape key uniformly.
import { OverlayProvider, overlay } from 'overlay-kit'; import * as Dialog from '@radix-ui/react-dialog'; function App() { return ( <button className="inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" onClick={() => { overlay.open(({ isOpen, close, unmount }) => ( <Dialog.Root open={isOpen} onOpenChange={(open) => { if (!open) { close(); // Radix dialog's close animation is around 150ms. setTimeout(unmount, 200); } }} > <Dialog.Portal> <Dialog.Overlay className="fixed inset-0 bg-black/50 transition-opacity data-[state=closed]:opacity-0 data-[state=open]:opacity-100" /> <Dialog.Content className="fixed left-1/2 top-1/2 z-50 grid w-full max-w-md -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg bg-white p-6 shadow-lg transition-opacity data-[state=closed]:opacity-0 data-[state=open]:opacity-100"> <Dialog.Title className="text-lg font-semibold">Are you sure you want to continue?</Dialog.Title> <Dialog.Description className="text-sm text-gray-500">This action cannot be undone.</Dialog.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-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700" onClick={close} > Yes </button> </div> </Dialog.Content> </Dialog.Portal> </Dialog.Root> )); }} > Open Confirm Dialog </button> ); } export function Example() { return ( <OverlayProvider> <App /> </OverlayProvider> ); }