Popover

A popover is a small floating panel that opens from a button. It holds a bit more than a tooltip can, such as a short explanation, a few settings or a form.

import { Button } from "@/components/ui/button";
import {
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverDescription,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover";

export default function PopoverDemo() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="secondary" />}>Notifications</PopoverTrigger>
      <PopoverContent>
        <PopoverTitle>Notifications</PopoverTitle>
        <PopoverDescription>You are all caught up. Good job!</PopoverDescription>
        <PopoverClose render={<Button variant="secondary" className="mt-2 self-start" />}>Dismiss</PopoverClose>
      </PopoverContent>
    </Popover>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

import type { ComponentProps } from "react";
import { Popover as BasePopover } from "@base-ui/react/popover";
import { cx } from "@/lib/cx";
import { popupMotion, popupSurface } from "@/lib/styles";
import type { WithClass } from "@/lib/types";

export const Popover = BasePopover.Root;

// Renders a button by default. To open it from your own element, such as a Button, pass it as
// render: <PopoverTrigger render={<Button />}>.
export const PopoverTrigger = BasePopover.Trigger;

// side, sideOffset and align place the popover against its trigger; every other prop goes to the
// popup itself.
export function PopoverContent({
  side = "bottom",
  sideOffset = 8,
  align = "center",
  className,
  ...props
}: WithClass<ComponentProps<typeof BasePopover.Popup>> &
  Pick<ComponentProps<typeof BasePopover.Positioner>, "side" | "sideOffset" | "align">) {
  return (
    <BasePopover.Portal>
      <BasePopover.Positioner side={side} sideOffset={sideOffset} align={align} className="z-50">
        <BasePopover.Popup className={cx(popupSurface, popupMotion, "flex w-72 flex-col gap-2 p-4", className)} {...props} />
      </BasePopover.Positioner>
    </BasePopover.Portal>
  );
}

export function PopoverTitle({ className, ...props }: WithClass<ComponentProps<typeof BasePopover.Title>>) {
  return <BasePopover.Title className={cx("text-base font-semibold text-text", className)} {...props} />;
}

export function PopoverDescription({ className, ...props }: WithClass<ComponentProps<typeof BasePopover.Description>>) {
  return <BasePopover.Description className={cx("text-sm text-text/70", className)} {...props} />;
}

export const PopoverClose = BasePopover.Close;

Props

PropTypeDefaultWhat it does
side"top" | "right" | "bottom" | "left""bottom"On PopoverContent. Which side of the trigger it opens on. It flips to the other side if there is no room.
align"start" | "center" | "end""center"On PopoverContent. How it lines up along that side.
sideOffsetnumber8On PopoverContent. The gap in pixels between the trigger and the popover.
defaultOpenbooleanfalseStarts the popover open, when it manages its own state.
openboolean—Whether it is open, when you control it yourself.
onOpenChangefunction—Called with the new open state.
renderelement—On PopoverTrigger and PopoverClose. Uses your own element, such as a Button, instead of a plain button.

Every other prop goes to the Base UI Popover underneath. PopoverTitle and PopoverDescription name the panel for screen readers.

The thinking

A popover is for something small that belongs to one control and shouldn't take over the page. It stays out of the way until asked.

  • Built on Base UI: Base UI places the panel next to the trigger, flips it when there is no room, and handles the open state, the focus and the dismissal.
  • Popover or Dialog: A popover sits beside its trigger and leaves the page usable. A Dialog covers the page and asks for full attention.
  • Popover or Tooltip: A tooltip is a label that appears on hover. A popover opens on click and can hold buttons, links and fields.

Interface design

  • Surface: The shared overlay surface: the surface fill, a strong hairline, the lg radius of 8px and a soft shadow. No arrow.
  • Width: Fixed at 18rem, so every popover on the site feels like the same object.
  • Text: A semibold title and a dimmer description, in the same rhythm as a Dialog.

Interaction

  • Open: Click the trigger, or press Enter or Space when it has focus.
  • Close: Escape, a click outside, or a PopoverClose button.
  • Focus: Focus moves into the panel when it opens and returns to the trigger when it closes.
  • Motion: A 150ms fade and a small scale from the side it opens from. With reduced motion turned on it appears at once.

Empathy

  • State is announced: The trigger reports whether the popover is open.
  • Named: The panel is named by its title and described by its description. Always include a PopoverTitle.
  • Don't hide required steps: Anything people must do should be on the page, not behind a click.