macbetter

Presentation

Popover

Show a small task or more detail beside the control it belongs to.

"use client"

import { Button } from "macbetter/button"
import { Field, FieldLabel } from "macbetter/field"
import {
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverDescription,
  PopoverTitle,
  PopoverTrigger,
} from "macbetter/popover"
import { TextField } from "macbetter/text-field"
import { ShareIcon } from "lucide-react"

export default function Example() {
  return (
    <Popover presentation="auto">
      <PopoverTrigger render={<Button />}>
        <ShareIcon aria-hidden="true" />
        Share
      </PopoverTrigger>
      <PopoverContent>
        <PopoverTitle>Share “Q3 Roadmap”</PopoverTitle>
        <PopoverDescription>Anyone with the link can view it.</PopoverDescription>
        <div style={{ display: "grid", gap: 12, marginTop: 10, minWidth: 280 }}>
          <Field>
            <FieldLabel>Link</FieldLabel>
            <TextField readOnly defaultValue="macbetter.dev/d/q3-roadmap" />
          </Field>
          <div style={{ display: "flex", justifyContent: "flex-end" }}>
            <PopoverClose render={<Button variant="filled" />}>Copy Link</PopoverClose>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}

When to use it

Use a popover for something brief that belongs to one control: sharing a link, adjusting a few options, explaining a setting. It closes when people click elsewhere, so don’t put work in it that would be painful to lose. For a longer task, use a Dialog; for a list of commands, a Menu; for a few words on hover, a Tooltip.

Usage

import {
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverDescription,
  PopoverTitle,
  PopoverTrigger,
} from "macbetter/popover"

PopoverContent draws its arrow and grows out of the trigger. It opens below the trigger and flips or shifts when there isn’t room; the arrow keeps pointing at the trigger. Give it a PopoverTitle to name it.

Placement

side chooses the edge (top, right, bottom, left) and align the alignment along it. Pass arrow={false} for a plain panel.

"use client"

import { Button } from "macbetter/button"
import { Popover, PopoverContent, PopoverDescription, PopoverTitle, PopoverTrigger } from "macbetter/popover"
import { InfoIcon } from "lucide-react"

export default function Example() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="plain" shape="circle" aria-label="About sync" />}>
        <InfoIcon aria-hidden="true" />
      </PopoverTrigger>
      <PopoverContent side="right">
        <PopoverTitle>iCloud sync</PopoverTitle>
        <PopoverDescription>Changes appear on your other devices within a few seconds.</PopoverDescription>
      </PopoverContent>
    </Popover>
  )
}

On a phone

iPhone has no room for a floating panel. Pass presentation="auto" to the root and it becomes a sheet you can swipe away on small touch screens, while iPad and the Mac keep the popover. The Share example at the top of this page does this: open it on a phone to see the sheet.

Platform behavior

  • macOS. Thick material with 12pt corners, 13pt text, and the arrow pointing at the control. It scales up from the trigger on a quick spring and fades out faster than it came in.
  • iPadOS. The same shape with larger corners, 17pt text and touch-size controls.
  • iOS. Popovers are rare on iPhone; use presentation="auto" to get a bottom sheet with a grabber there.
  • The arrow blurs the page behind it just as the panel does, so the two read as one surface.

Accessibility

  • The content has the dialog role, named by its title and described by its description.
  • Focus moves into it on open. Escape, a press outside, or focus leaving it closes it, and focus returns to the trigger.
  • modal traps focus and makes the page inert when the popover holds a task that shouldn’t be interrupted.

API

Popover

Takes every ui-sdk Popover root prop, like open, onOpenChange, modal, openOnHover and presentation.

PopoverContent

PropTypeDefaultDescription
arrowbooleantrueDraws the arrow that points at the trigger.
side"top" | "right" | "bottom" | "left""bottom"The edge of the trigger it opens on.
align"start" | "center" | "end""center"How it lines up along that edge.
sideOffsetnumber12 with an arrow, 6 withoutThe gap to the trigger.
arrowPaddingnumber14Keeps the arrow clear of the corners.

PopoverTrigger, PopoverTitle, PopoverDescription and PopoverClose take ui-sdk’s props. Render a macbetter Button with render={<Button />}. createPopoverController is re-exported from ui-sdk.

Data attributeOnMeaning
data-side, data-align.mb-popover, .mb-popover-arrowWhere it landed
data-arrow.mb-popoverIt has an arrow
data-presentation.mb-popoverpopover or sheet
data-starting-style, data-ending-style.mb-popoverEntering and leaving
CSS variableDefault
--mb-popover-radius--mb-radius-lg on macOS, --mb-popup-radius on touch
--mb-popover-padding14px on macOS, 16px on touch
--mb-popover-max-width320px on macOS, 360px on touch
--transform-origin, --arrow-offsetSet by ui-sdk