OverlayManager
OverlayManager opens overlay components imperatively from anywhere in your app.
Usage#
import { createOverlayManager } from '@primereact/core/overlay-manager';Declare a manager at module scope, pass a render function that spreads the injected props onto an overlay root, and mount <manager.Viewport /> once in your layout.
const dialog = createOverlayManager<{ title: string; description?: string }>(({ title, description, ...rest }) => (
<Dialog.Root {...rest}>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Popup>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
</Dialog.Header>
<Dialog.Content>{description}</Dialog.Content>
</Dialog.Popup>
</Dialog.Positioner>
</Dialog.Portal>
</Dialog.Root>
));The render function receives the caller props merged with three injected lifecycle props — open, onOpenChange, and onExitComplete. Spreading them onto Dialog.Root lets the manager fully control open state, entry into the tree, and removal after the exit animation.
// Layout
<dialog.Viewport />;
// Anywhere
dialog.open({ title: 'Hello', description: 'World' });API#
interface OverlayManagerInstance<P> {
open: (props: P, id?: string) => string;
close: (id?: string) => void;
update: (id: string, props: Partial<P>) => void;
Viewport: React.ComponentType;
}open(props, id?)— mounts a new entry with the given props. Returns the generated (or supplied) id.close(id?)— starts the close transition. Called with no id, every open entry is closed.update(id, props)— shallow-merges new props into an already-open entry. The mounted overlay re-renders in place.Viewport— the component that renders every open entry. Mount once per manager.
Examples#
Basic#
Open a dialog imperatively with open() and close it via the close button or escape key.
'use client';
import { Times } from '@primeicons/react/times';
import { createOverlayManager } from '@primereact/core/overlay-manager';
import { Button } from '@primereact/ui/button';
import { Dialog } from '@primereact/ui/dialog';
interface DialogPayload {
title: string;
description: string;
}
const dialog = createOverlayManager<DialogPayload>(({ title, description, ...rest }) => (
<Dialog.Root {...rest}>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Popup style={{ width: '24rem' }}>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.HeaderActions>
<Dialog.Close as={Button} rounded variant="text" iconOnly>
<Times />
</Dialog.Close>
</Dialog.HeaderActions>
</Dialog.Header>
<Dialog.Content>
<p className="text-sm text-muted-color m-0">{description}</p>
</Dialog.Content>
</Dialog.Popup>
</Dialog.Positioner>
</Dialog.Portal>
</Dialog.Root>
));
export default function BasicDemo() {
return (
<div className="flex justify-center">
<Button onClick={() => dialog.open('basic', { title: 'Hello', description: 'This dialog was opened imperatively.' })}>Open dialog</Button>
<dialog.Viewport />
</div>
);
}
Update#
Call update(id, partialProps) after open to change the mounted dialog's content without closing it.
'use client';
import { Times } from '@primeicons/react/times';
import { createOverlayManager } from '@primereact/core/overlay-manager';
import { Button } from '@primereact/ui/button';
import { Dialog } from '@primereact/ui/dialog';
interface DialogPayload {
title: string;
description: string;
}
const dialog = createOverlayManager<DialogPayload>(({ title, description, ...rest }) => (
<Dialog.Root {...rest}>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Popup style={{ width: '24rem' }}>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.HeaderActions>
<Dialog.Close as={Button} rounded variant="text" iconOnly>
<Times />
</Dialog.Close>
</Dialog.HeaderActions>
</Dialog.Header>
<Dialog.Content>
<p className="text-sm text-muted-color m-0">{description}</p>
</Dialog.Content>
</Dialog.Popup>
</Dialog.Positioner>
</Dialog.Portal>
</Dialog.Root>
));
export default function UpdateDemo() {
const openAndUpdate = () => {
const id = 'sync';
dialog.open(id, {
title: 'Syncing…',
description: 'This dialog will update itself in 2 seconds.'
});
setTimeout(() => {
dialog.update(id, {
title: 'Synced',
description: `Completed at ${new Date().toLocaleTimeString()}`
});
}, 2000);
};
return (
<div className="flex justify-center">
<Button onClick={openAndUpdate}>Open + update</Button>
<dialog.Viewport />
</div>
);
}
Stacked#
Multiple entries mount side by side. close() with no id begins closing every open entry — each one finishes its own exit animation independently.
'use client';
import { Times } from '@primeicons/react/times';
import { createOverlayManager } from '@primereact/core/overlay-manager';
import { Button } from '@primereact/ui/button';
import { Dialog } from '@primereact/ui/dialog';
interface DialogPayload {
title: string;
description: string;
}
const dialog = createOverlayManager<DialogPayload>(({ title, description, ...rest }) => (
<Dialog.Root {...rest}>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Popup style={{ width: '22rem' }}>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.HeaderActions>
<Dialog.Close as={Button} rounded variant="text" iconOnly>
<Times />
</Dialog.Close>
</Dialog.HeaderActions>
</Dialog.Header>
<Dialog.Content>
<p className="text-sm text-muted-color m-0">{description}</p>
</Dialog.Content>
</Dialog.Popup>
</Dialog.Positioner>
</Dialog.Portal>
</Dialog.Root>
));
export default function StackedDemo() {
const openThree = () => {
dialog.open('first', { title: 'First', description: 'Opened first.' });
dialog.open('second', { title: 'Second', description: 'Opened right after.' });
dialog.open('third', { title: 'Third', description: 'All three are mounted together.' });
};
return (
<div className="flex justify-center gap-2">
<Button onClick={openThree}>Open three</Button>
<dialog.Viewport />
</div>
);
}
Confirm#
Pass user-defined callbacks (like onConfirm) as part of your payload — they remain available inside the render function.
'use client';
import { Times } from '@primeicons/react/times';
import { createOverlayManager } from '@primereact/core/overlay-manager';
import { Button } from '@primereact/ui/button';
import { Dialog } from '@primereact/ui/dialog';
import * as React from 'react';
interface ConfirmPayload {
title: string;
description: string;
confirmLabel?: string;
onConfirm?: () => void;
}
const confirm = createOverlayManager<ConfirmPayload>(({ title, description, confirmLabel = 'Confirm', onConfirm, ...rest }) => (
<Dialog.Root {...rest}>
<Dialog.Portal>
<Dialog.Backdrop />
<Dialog.Positioner>
<Dialog.Popup style={{ width: '22rem' }}>
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.HeaderActions>
<Dialog.Close as={Button} rounded variant="text" iconOnly>
<Times />
</Dialog.Close>
</Dialog.HeaderActions>
</Dialog.Header>
<Dialog.Content>
<div className="flex flex-col gap-6">
<p className="text-sm text-muted-color m-0">{description}</p>
<div className="flex justify-end gap-2">
<Dialog.Close as={Button} severity="secondary">
Cancel
</Dialog.Close>
<Dialog.Close as={Button} severity="danger" onClick={() => onConfirm?.()}>
{confirmLabel}
</Dialog.Close>
</div>
</div>
</Dialog.Content>
</Dialog.Popup>
</Dialog.Positioner>
</Dialog.Portal>
</Dialog.Root>
));
export default function ConfirmDemo() {
const [status, setStatus] = React.useState<string | null>(null);
return (
<div className="flex flex-col items-center gap-3">
<Button
severity="danger"
onClick={() =>
confirm.open('delete', {
title: 'Delete item?',
description: 'This action cannot be undone.',
confirmLabel: 'Delete',
onConfirm: () => setStatus('Item deleted')
})
}
>
Delete
</Button>
{status ? <span className="text-xs text-muted-color">{status}</span> : null}
<confirm.Viewport />
</div>
);
}
Drawer#
The same API works for any overlay component that accepts open / onOpenChange / onOpenChangeComplete — here a managed Drawer with configurable position.
'use client';
import { Times } from '@primeicons/react/times';
import { createOverlayManager } from '@primereact/core/overlay-manager';
import type { useDrawerProps } from '@primereact/types/headless/drawer';
import { Button } from '@primereact/ui/button';
import { Drawer } from '@primereact/ui/drawer';
interface DrawerPayload {
title: string;
description: string;
position?: useDrawerProps['position'];
}
const drawer = createOverlayManager<DrawerPayload>(({ title, description, ...rest }) => (
<Drawer.Root {...rest}>
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Popup className="w-full md:w-80">
<Drawer.Header>
<Drawer.Title>{title}</Drawer.Title>
<Drawer.Close as={Button} rounded variant="text" iconOnly>
<Times />
</Drawer.Close>
</Drawer.Header>
<Drawer.Content>
<p className="text-sm text-muted-color m-0">{description}</p>
</Drawer.Content>
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>
));
export default function DrawerDemo() {
return (
<div className="flex flex-wrap justify-center gap-2">
<Button
onClick={() => drawer.open('left', { title: 'Notifications', description: 'You have 3 unread notifications.', position: 'left' })}
>
Open left
</Button>
<Button
variant="outlined"
onClick={() => drawer.open('right', { title: 'Cart', description: 'Your cart is currently empty.', position: 'right' })}
>
Open right
</Button>
<drawer.Viewport />
</div>
);
}
Popover#
Popovers need an anchor element. Pass a ref to it through the payload and read it inside the render function. Popover's onOpenChange uses { open } instead of { value }, so the render function bridges the two.
'use client';
import { createOverlayManager } from '@primereact/core/overlay-manager';
import { Button } from '@primereact/ui/button';
import { Popover } from '@primereact/ui/popover';
import type * as React from 'react';
interface PopoverPayload {
anchor: HTMLElement | null;
title: string;
description: string;
}
const popover = createOverlayManager<PopoverPayload>(({ title, description, ...rest }) => (
<Popover.Root {...rest}>
<Popover.Portal>
<Popover.Positioner sideOffset={8} side="bottom" align="start">
<Popover.Popup className="max-w-64 w-full">
<Popover.Arrow />
<Popover.Header>
<Popover.Title>{title}</Popover.Title>
</Popover.Header>
<Popover.Content>
<Popover.Description>{description}</Popover.Description>
</Popover.Content>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
));
export default function PopoverDemo() {
return (
<div className="flex justify-center">
<Button
onClick={(e: React.MouseEvent<HTMLButtonElement>) =>
popover.open('shortcut', {
anchor: e.currentTarget,
title: 'Shortcut',
description: 'Press ⌘K to open the command palette.'
})
}
>
Show popover
</Button>
<popover.Viewport />
</div>
);
}
Tooltip#
Tooltip accepts an anchor prop for imperative use, just like Popover. Opening a tooltip from a click doesn't feel native — prefer Popover or Toast for click-driven flows — but the plumbing works identically when you need it.
'use client';
import { createOverlayManager } from '@primereact/core/overlay-manager';
import { Button } from '@primereact/ui/button';
import { Tooltip } from '@primereact/ui/tooltip';
import type * as React from 'react';
interface TooltipPayload {
anchor: HTMLElement | null;
description: string;
}
const tooltip = createOverlayManager<TooltipPayload>(({ description, ...rest }) => (
<Tooltip.Root {...rest} disabled openDelay={0} closeDelay={0}>
<Tooltip.Portal>
<Tooltip.Positioner sideOffset={8}>
<Tooltip.Popup className="max-w-56">
<Tooltip.Arrow />
{description}
</Tooltip.Popup>
</Tooltip.Positioner>
</Tooltip.Portal>
</Tooltip.Root>
));
export default function TooltipDemo() {
const triggers = [
{ label: 'First', description: 'Saved to your library.' },
{ label: 'Second', description: 'Shared across the team.' },
{ label: 'Third', description: 'Archived after 30 days.' }
];
return (
<Tooltip.Manager>
<div className="flex justify-center gap-2">
{triggers.map((t) => (
<Button
key={t.label}
onPointerEnter={(e: React.PointerEvent<HTMLButtonElement>) =>
tooltip.open('shared', { anchor: e.currentTarget, description: t.description })
}
onPointerLeave={() => tooltip.close('shared')}
>
{t.label}
</Button>
))}
<tooltip.Viewport />
</div>
</Tooltip.Manager>
);
}