useOverlay
Provides the anchor and panel refs, controllable open state, outside-click and Escape dismissal, and the aria pair on the trigger.
Signature
useOverlay returns anchor and panel refs, controllable open state, dismissal handlers, and trigger accessibility attributes.
function useOverlay<A extends HTMLElement = HTMLElement>(options?: {
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
/** `aria-haspopup` on the trigger. `false` for a tooltip, which is not a popup. */
haspopup?: "menu" | "listbox" | "dialog" | "tree" | "grid" | false;
/** Outside pointerdown and Escape close it. Default true. */
dismissable?: boolean;
disabled?: boolean;
/** Leaves `onClick` off `triggerProps` — for hover- or focus-driven overlays. */
manual?: boolean;
}): {
open: boolean;
setOpen: (open: boolean) => void;
toggle: () => void;
close: () => void;
anchorRef: { current: A | null };
panelRef: { current: HTMLDivElement | null };
/** Spread onto the trigger element. */
triggerProps: { ref; onClick?; "aria-haspopup"?; "aria-expanded"? };
/** Spread into `Popover` — placement, offset and matchWidth stay yours. */
popoverProps: { open; anchorRef; panelRef };
};Usage
Spread triggerProps onto the trigger element and popoverProps into Popover.
import type { ReactNode } from "react";
import { useOverlay, Popover, Surface } from "@solarius/onset";
function FilterMenu({ children }: { children: ReactNode }) {
const { open, triggerProps, popoverProps } = useOverlay<HTMLButtonElement>({
haspopup: "menu",
});
return (
<>
<button {...triggerProps} data-state={open ? "open" : "closed"}>
Filters
</button>
<Popover {...popoverProps} placement="bottom-end" offset={6}>
<Surface elevation="overlay">{children}</Surface>
</Popover>
</>
);
}Pass manual for an overlay that is not opened by a click. HoverCard and Tooltip both do: they use the refs, state, and aria pair but supply their own pointer handlers. Tooltip additionally passes haspopup: false, because a tooltip is not a popup, and dismissable: false, because it appears and disappears with the pointer rather than with a click elsewhere.
Closing side effects
Use onOpenChange to reset temporary state when an overlay closes. This callback receives state changes from each close action.
const { open, setOpen, triggerProps, popoverProps } = useOverlay({
haspopup: "dialog",
onOpenChange: (next) => {
if (!next) setDraft(null);
},
});