Popover

Popover allows to create dialog that shows next to the trigger element.

Source
Theme Source
pnpm dlx dreamy add popover

When Popover opens, focus is sent to PopoverContent. When it closes, focus is returned to the trigger.

If you want to show arrow, you can use hasArrow prop.

export function BasicPopover() {
    return (
        <Popover.Root hasArrow>
            <Popover.Trigger>
                <Button variant={"primary"}>Open Popover</Button>
            </Popover.Trigger>
 
            <Popover.Content>
                <Popover.CloseButton />
                <Popover.Header>Delete Post</Popover.Header>
                <Popover.Body>
                    Are you sure you want to delete this post? This action cannot be undone.
                </Popover.Body>
                <Popover.Footer>
                    <Button variant={"primary"}>Delete</Button>
                </Popover.Footer>
            </Popover.Content>
        </Popover.Root>
    );
}

Popover content renders through the shared Portal stack at the popover layer (1500). Inside another Dreamy UI overlay, its portal becomes a nested scope and adds that layer to its parent base automatically. No usePortal={false}, manual layer, or z-index workaround is needed.

Active popovers at the same layer follow open order. A later sibling Modal still covers an earlier Modal's complete subtree, including open popovers.

Popover-based composites expose the same escape hatch through popoverProps.portalProps. Use portalProps.containerRef only to target the non-scrolling overlay root of a native or third-party overlay.

const overlayRef = useRef<HTMLDivElement>(null);
 
// Attach overlayRef to the third-party overlay's non-scrolling root.
<Popover.Root portalProps={{ containerRef: overlayRef }}>
    <Popover.Trigger>
        <Button>Open Popover</Button>
    </Popover.Trigger>
    <Popover.Content>Rendered in the external overlay root</Popover.Content>
</Popover.Root>

Most of the time, you'll want to control the popover's open state. Use useControllable hook to do that.

export function ControlledPopover() {
    const { isOpen, onOpen, onClose } = useControllable();
 
    const handleDelete = useCallback(() => {
        /**
         * Handle delete logic...
         */
        onClose();
    }, [onClose]);
 
    return (
        <Popover.Root hasArrow isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
            <Popover.Trigger>
                <Button variant={"primary"}>Open Popover</Button>
            </Popover.Trigger>
 
            <Popover.Content>
                <Popover.CloseButton />
                <Popover.Header>Delete Post</Popover.Header>
                <Popover.Body>
                    Are you sure you want to delete this post? This action cannot be undone.
                </Popover.Body>
                <Popover.Footer>
                    <Button variant={"solid"} onClick={onClose}>
                        Cancel
                    </Button>
                    <Button variant={"primary"} onClick={handleDelete}>
                        Delete
                    </Button>
                </Popover.Footer>
            </Popover.Content>
        </Popover.Root>
    );
}

You can set the initial focus element using initialFocusRef prop.

export function FocusPopover() {
    const { isOpen, onOpen, onClose } = useControllable();
    const initialFocusRef = useRef<HTMLButtonElement>(null);
 
    return (
        <Popover.Root
            hasArrow
            isOpen={isOpen}
            onOpen={onOpen}
            onClose={onClose}
            initialFocusRef={initialFocusRef}
        >
            <Popover.Trigger>
                <Button variant={"primary"}>Open Popover</Button>
            </Popover.Trigger>
 
            <Popover.Content>
                <Popover.CloseButton />
                <Popover.Header>Delete Post</Popover.Header>
                <Popover.Body>
                    Are you sure you want to delete this post? This action cannot be undone.
                </Popover.Body>
                <Popover.Footer>
                    <Button variant={"solid"} onClick={onClose} ref={initialFocusRef}>
                        Cancel
                    </Button>
                    <Button variant={"primary"}>Delete</Button>
                </Popover.Footer>
            </Popover.Content>
        </Popover.Root>
    );
}

You can customize the placement of the popover relative to the trigger element using the positioning prop.

const placements = [
    "top", "bottom", "left", "right",
    "top-start", "top-end", "bottom-start", "bottom-end",
    "left-start", "left-end", "right-start", "right-end"
];
 
export function PlacementPopovers() {
    return (
        <Flex wrapped gap={5}>
            {placements.map((placement) => (
                <PlacementPopover key={placement} placement={placement} />
            ))}
        </Flex>
    );
}
 
export function PlacementPopover({ placement }: { placement: string }) {
    return (
        <Popover.Root hasArrow positioning={{ placement }}>
            <Popover.Trigger>
                <Button variant={"primary"}>{placement}</Button>
            </Popover.Trigger>
 
            <Popover.Content>
                <Popover.CloseButton />
                <Popover.Header>Delete Post</Popover.Header>
                <Popover.Body>
                    Are you sure you want to delete this post? This action cannot be undone.
                </Popover.Body>
                <Popover.Footer>
                    <Button variant={"primary"}>Delete</Button>
                </Popover.Footer>
            </Popover.Content>
        </Popover.Root>
    );
}

Use size prop to set the size of the popover.

export function SizePopovers() {
    return (
        <Flex
            wrapped
            gap={5}
        >
            {(
                ["sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "8xl"]
            ).map((size) => (
                <SizePopover key={size} size={size} />
            ))}
        </Flex>
    );
}
 
export function SizePopover({ size }: { size: string }) {
    return (
        <Popover.Root hasArrow size={size as any}>
            <Popover.Trigger>
                <Button
                    variant={"primary"}
                    w="fit-content"
                >
                    {size}
                </Button>
            </Popover.Trigger>
 
            <Popover.Content>
                <Popover.CloseButton />
                <Popover.Header>Delete Post</Popover.Header>
                <Popover.Body>
                    Are you sure you want to delete this post? This action cannot be undone.
                </Popover.Body>
                <Popover.Footer>
                    <Button variant={"primary"}>Delete</Button>
                </Popover.Footer>
            </Popover.Content>
        </Popover.Root>
    );
}

You can customize the reduce motion behavior of the popover using the reduceMotion prop.

<Popover.Root reduceMotion hasArrow>
    <Popover.Trigger>
        <Button variant={"primary"}>Reduced Motion</Button>
    </Popover.Trigger>
 
    <Popover.Content>
        <Popover.CloseButton />
        <Popover.Header>Delete Post</Popover.Header>
        <Popover.Body>
            Are you sure you want to delete this post? This action cannot be undone.
        </Popover.Body>
        <Popover.Footer>
            <Button variant={"primary"}>Delete</Button>
        </Popover.Footer>
    </Popover.Content>
</Popover.Root>