Anchored Toasts
Pin toasts to elements, clicks, or coordinates. The thing Sonner still hasn't shipped.
Pin a toast to a DOM element, a mouse click, or arbitrary coordinates. Built on Floating UI via Base UI. Tooltips with commitment issues.
One rule: only one anchored toast at a time. New one boots the previous one.
Usage
import { toast } from "@vcui/popser";
// anchor to an element
toast.success("Copied!", {
anchor: buttonRef.current,
timeout: 2000,
});
// anchor to a click
function handleClick(event: React.MouseEvent) {
toast.info("Context menu", { anchor: event.nativeEvent });
}
// anchor to coordinates
toast.warning("Here", { anchor: { x: 200, y: 300 } });Anchor types
| Type | What it does |
|---|---|
Element | DOM element — ref, querySelector, whatever |
MouseEvent | Appears exactly where you clicked |
{ x: number, y: number } | Arbitrary screen coordinates |
null | Regular viewport toast, nothing special |
For MouseEvent and {x, y}, popser creates a temporary fixed-position element at those coordinates and cleans it up when the toast closes. No orphaned divs. We're civilised.
Click-anchored
Toast appears right where the click happened. Pass the native event.
function handleClick(event: React.MouseEvent) {
toast.success("Right here", {
anchor: event.nativeEvent,
anchorOffset: 16,
arrow: true,
});
}Positioning
Control where the toast sits relative to the anchor. Try all four sides:
toast.success("Saved", {
anchor: element,
anchorSide: "top",
anchorAlign: "center",
anchorOffset: 12,
});Side
| Value | Where |
|---|---|
"top" | Above |
"bottom" | Below (default) |
"left" | Left |
"right" | Right |
"inline-start" | Logical inline start |
"inline-end" | Logical inline end |
Alignment
| Value | Where |
|---|---|
"start" | Start edge |
"center" | Centre (default) |
"end" | End edge |
Offset
{
anchorOffset: 8, // distance from anchor (default: 8px)
anchorAlignOffset: 0, // offset along alignment axis (default: 0)
}Arrow
Show an arrow pointing at the anchor. Compare with and without:
toast.success("Copied!", {
anchor: buttonRef.current,
anchorSide: "top",
arrow: true,
arrowPadding: 5,
});Inherits toast colours. Styled via data-popser-arrow and data-side.
Collision handling
Toasts flip and shift to stay in the viewport. No clipping off the edge like it's 2008.
{
anchorCollisionBoundary: "clipping-ancestors", // default
anchorCollisionPadding: 5,
}Pass a specific element or array if the default boundary isn't enough.
Position method
{
anchorPositionMethod: "absolute", // default
anchorPositionMethod: "fixed", // for scrollable containers
}"absolute" positions relative to nearest positioned ancestor. "fixed" pins to the viewport. Use "fixed" when your anchor sits inside a scrollable container and you want the toast to stay put.
Sticky
Keep the toast visible when the anchor scrolls away. The toast stays. The anchor runs.
{
anchorSticky: true,
}Full example
function CopyButton() {
const ref = useRef<HTMLButtonElement>(null);
async function handleCopy() {
await navigator.clipboard.writeText("copied text");
toast.success("Copied to clipboard", {
anchor: ref.current,
anchorSide: "top",
anchorAlign: "center",
anchorOffset: 8,
arrow: true,
timeout: 2000,
dismissible: false,
});
}
return (
<button ref={ref} onClick={handleCopy}>
Copy
</button>
);
}Data attributes
Anchored toasts get data-anchored on the root:
[data-popser-root][data-anchored] {
/* anchored toast styles */
}
[data-popser-arrow] {
/* arrow element */
}
[data-popser-arrow][data-side="top"] {
/* arrow when toast is above */
}