Popover

A floating panel beside its trigger, for details or a small form.

StableSince 1.0.0
On this page

Import

import { Popover } from '@envsemble/envui';

Usage

A Popover opens from its trigger, such as a Button or an IconButton, which must take a ref and props: it receives the press handler, aria-haspopup and aria-expanded. title is a heading at the top that names it; without one, name it with aria-label. children is its content.

DocumentDetails.tsx
import { Button, Popover, Column, Text } from '@envsemble/envui';
import { IconInfo } from '@envsemble/envui/icons/Info';

export default function DocumentDetails() {
  return (
    <Column gap={4} align="start">
      <Popover
        // Shown open in the docs; without defaultOpen, it opens from its trigger.
        defaultOpen
        title="Launch plan.md"
        trigger={
          <Button variant="secondary" startIcon={<IconInfo />}>
            Details
          </Button>
        }
      >
        <Text emphasis="muted">Changed today by Alex Morgan. 3 open decisions, 1 blocked.</Text>
        <Text emphasis="muted">Shared with everyone at Northwind.</Text>
      </Popover>
    </Column>
  );
}

A small form

A function child receives close, for an action inside that closes the popover once it is done, such as a form's submit.

<Popover aria-label="Rename document" width="lg" trigger={<Button>Rename</Button>}>
  {(close) => (
    <form onSubmit={(event) => rename(event, close)}>
      <TextField label="Name" name="name" required />
      <Button variant="secondary" onClick={close}>
        Cancel
      </Button>
      <Button type="submit">Rename</Button>
    </form>
  )}
</Popover>

Controlled

Leave it uncontrolled, with defaultOpen to start open, or control it with open and onOpenChange, which receives the requested state and why: the trigger, Esc, a press outside, focus leaving it, or close as close-press.

<Popover
  title="Assignees"
  open={open}
  onOpenChange={(next) => {
    setOpen(next);
    if (next) setDraft(applied); // Each time it opens, start from the filters in use.
  }}
  trigger={<Button startIcon={<IconFilterList />}>Assignees</Button>}
>
  {people.map((person) => (
    <Checkbox
      key={person}
      label={person}
      checked={draft.includes(person)}
      onCheckedChange={(checked) => toggle(person, checked)}
    />
  ))}
  <Button
    onClick={() => {
      setApplied(draft);
      setOpen(false);
    }}
  >
    Apply
  </Button>
</Popover>

Width, side and alignment

width sets how wide the popover grows with its content: md, the default, from 12 rem up to 20 rem, or lg, from 20 rem up to 28 rem; it narrows to fit the window. side is bottom, the default, top, left, right, or inline-start and inline-end, which follow the text direction; it flips to the other side without room. align is start, center, the default, or end.

<Popover
  title="Billing periods"
  side="right"
  align="start"
  trigger={<IconButton variant="text" icon={<IconHelp />} aria-label="About billing periods" />}
>
  <Text>Members added later are billed from the next period.</Text>
</Popover>

Customize

Every popup, a popover, a menu, a Select's or a Combobox's list, a picker, a hover card, rich text suggestions and the command palette, is frosted. Three properties, set on a .env-theme boundary, change them all:

  • --env-popup-opacity, 88 % in light and 90 % in dark, how much of the popup's color covers what is behind it; 100% makes popups solid. The color is --env-color-bg-popup.
  • --env-popup-blur, 1.375 rem, the blur of what shows through, with a little more saturation.
  • --env-popup-shadow, a soft shadow in the text's color in light and a deeper shade of the page in dark: with the avatars', envUI's only shadow.

In dark a faint hairline of the text color keeps a frosted popup's edge, where its shadow barely shows. At high contrast, with reduced transparency and under forced colors popups are solid, with their elevation edge and no blur or shadow; at high contrast the edge is a modal's, --env-elevation-3-border, the strongest. Tooltips are frosted too, at --env-tooltip-opacity with a smaller --env-tooltip-shadow, as Tooltip describes.

className, style, ref and data attributes reach the surface, env-popover, which has data-width="lg" when large and renders on PopupSurface in the trigger's text direction and theme. Its title is env-popover-title.

Accessibility

You provide

  • A title or an aria-label, and a name for an icon-only trigger.
  • The actions inside, and a call to close when one is done.

envUI provides

  • aria-haspopup="dialog" and aria-expanded on the trigger, and a dialog named by its title.
  • Focus moved in on open, and back to the trigger when Esc or close closes it.
  • The theme of its trigger in the portal.

Props

Popover

A floating panel beside its trigger, for details or a small form: it opens on press, moves focus into itself, and closes with Esc, a press outside or when focus leaves it. className, style, ref and data attributes go to the popover's surface.

PropTypeDescription
triggerrequired
ReactElement
The control that opens and closes the popover, such as a Button or an IconButton. It must take a ref and props: it receives the press handler, aria-haspopup and aria-expanded.
title
ReactNode
A heading at the top, such as the name of what the popover describes; it names the popover.
childrenrequired
ReactNode((close: () => void) => ReactNode)
The popover's content, or a function that receives close, for an action inside it that should close the popover once it is done, such as a form's submit.
aria-label
string
The popover's accessible name when it has no title, such as Rename document.
sideDefault 'bottom'
'top''bottom''left''right''inline-start''inline-end'
The side of the trigger the popover opens on: top, bottom, left, right, or inline-start and inline-end, which follow the text direction. It flips to the other side without room.
alignDefault 'center'
'start''center''end'
How the popover lines up with the trigger along that side: its start, center or end.
open
boolean
Whether a controlled popover is open; update it from onOpenChange.
defaultOpenDefault false
boolean
Whether an uncontrolled popover starts open.
onOpenChange
(open: boolean, details: PopoverOpenChangeDetails) => void
Receives the requested state, and why, each time the popover asks to open or close: from the trigger, Esc, a press outside, focus leaving it or close (`close-press`).
widthDefault 'md'
'md''lg'
The widest the popover grows: md, 20 rem, for details and a field or two; lg, 28 rem, for a longer form. It is as wide as its content from 12 rem up, and narrows to fit the window.
skeletonDefault false
boolean
Shows it and its content as a busy skeleton while that loads; turning it on or off renders the content anew.

Also accepts the native props of <div>, such as ref, className and aria-* attributes.

Part names

Each part's class and state attributes are public and versioned, for an application's CSS to style.

env-popover
The popover.
env-popover-title
The popover's title.
data-width
lg

Changelog

1.0.0

width is md or lg, the widest the popover grows as it sizes to its content; side takes inline-start and inline-end; className, style, ref and data attributes reach the surface, which renders on PopupSurface in the trigger's text direction.

Popovers take the level-2 elevation edge, with no shadow.

1.0.0-prerelease.6

A popover opens over --env-motion-duration-medium, fading in and growing from 98 % with --env-motion-ease-enter, rather than growing from 96 % over the slow duration, so it shows from its first frames rather than late.

A popover's corners are the popups' own, concentric with their rows and half a row's height, 20 px and 16 px at the compact scale, rather than a surface's 28 px and 22 px; dialogs and sheets keep a surface's.

1.0.0-prerelease.5

Popovers are frosted: the popup's color lets a little of what is behind through, blurred, with a soft shadow and no outline edge but a faint hairline in dark, and is solid with its edge at high contrast and with reduced transparency: --env-popup-opacity, --env-popup-blur and --env-popup-shadow. Its inset is a panel's, as wide as a popup row's corner: 14 px, 11.2 px at the compact scale.