Popover

Floating panel with controlled open state and auto-close support.

API Reference

PropTypeDefault
openboolean
onOpenChange(open: boolean) => void
contentReactNode
childrenReactElement
side"top" | "bottom" | "left" | "right""top"
sideOffsetnumber8
autoClosenumber

Sides

<Popover side="top" ...>...</Popover>
<Popover side="bottom" ...>...</Popover>
<Popover side="left" ...>...</Popover>
<Popover side="right" ...>...</Popover>

Auto Close

Pass a duration in milliseconds to automatically close the popover after it opens.

<Popover
  open={open}
  onOpenChange={setOpen}
  content="Closes in 2s"
  autoClose={2000}
>
  <IconButton
    variant="ghost"
    aria-label="Auto close"
    onClick={() => setOpen(true)}
  >
    <InfoIcon />
  </IconButton>
</Popover>

Usage Notes

  • This is a pre-styled wrapper around Base UI Popover. Refer to their docs for the full API.
  • The trigger wraps children in a span — pass any interactive element as the child.

Basic Usage

import { Popover, Button } from "@/ui";

<Popover content={<p>Popover content</p>}>
  <Button>Open</Button>
</Popover>

Source

src/ui/Popover.tsx
"use client";

import { useEffect } from "react";
import { Popover as BasePopover } from "@base-ui/react/popover";

type Side = "top" | "bottom" | "left" | "right";

interface PopoverProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  content: React.ReactNode;
  children: React.ReactElement;
  side?: Side;
  sideOffset?: number;
  autoClose?: number;
}

export function Popover({
  open,
  onOpenChange,
  content,
  children,
  side = "top",
  sideOffset = 8,
  autoClose,
}: PopoverProps) {
  useEffect(() => {
    if (!open || !autoClose) return;
    const id = setTimeout(() => onOpenChange(false), autoClose);
    return () => clearTimeout(id);
  }, [open, autoClose, onOpenChange]);

  return (
    <BasePopover.Root open={open} onOpenChange={onOpenChange}>
      <BasePopover.Trigger
        render={<span className="inline-flex" />}
        nativeButton={false}
      >
        {children}
      </BasePopover.Trigger>
      <BasePopover.Portal>
        <BasePopover.Positioner
          side={side}
          sideOffset={sideOffset}
          className="z-60"
        >
          <BasePopover.Popup className="rounded-md bg-root-contrast px-3 py-1.5 text-xs text-root opacity-0 transition-[opacity,transform] duration-150 data-exiting:opacity-0 data-open:opacity-100 data-[side=bottom]:data-exiting:translate-y-1 data-[side=bottom]:data-open:translate-y-0 data-[side=top]:data-exiting:-translate-y-1 data-[side=top]:data-open:translate-y-0">
            {content}
          </BasePopover.Popup>
        </BasePopover.Positioner>
      </BasePopover.Portal>
    </BasePopover.Root>
  );
}