Popser

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

TypeWhat it does
ElementDOM element — ref, querySelector, whatever
MouseEventAppears exactly where you clicked
{ x: number, y: number }Arbitrary screen coordinates
nullRegular 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

ValueWhere
"top"Above
"bottom"Below (default)
"left"Left
"right"Right
"inline-start"Logical inline start
"inline-end"Logical inline end

Alignment

ValueWhere
"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 */
}

On this page