Gecko UIGecko UI

Popover

A panel anchored to whatever opens it

Popover

A panel anchored to whatever opens it, holding anything you like.

Installation

import { Popover, PopoverTrigger, PopoverContent } from '@geckoui/geckoui';

Basic Usage

<Popover>
  <PopoverTrigger>
    <Button>Open</Button>
  </PopoverTrigger>

  <PopoverContent>
    <p>Anything can live in here.</p>
  </PopoverContent>
</Popover>

PopoverTrigger uses its child as the trigger rather than wrapping it, so your button keeps its own tag, styling and click handler.

Props API

Popover

PropTypeDefaultDescription
openboolean-Whether it is showing. Leave it out to let the component hold the state
defaultOpenbooleanfalseWhether it starts open when uncontrolled
onOpenChange(open: boolean) => void-Called whenever it opens or closes
placementPlacement'bottom-start'Where the panel sits. It flips when there is no room
offsetnumber6The gap between the trigger and the nearest part of the popover, in pixels
floatingStrategyStrategy-Positioning strategy, for the rare case absolute does not work
dismissOnEscapebooleantrueClose on Escape
dismissOnOutsideClickbooleantrueClose when something outside is clicked
arrowbooleanfalseShow a small arrow pointing at the trigger
disabledbooleanfalseStop it opening at all
...restHTMLAttributes<HTMLDivElement>-Spread onto the container

PopoverTrigger

PropTypeDefaultDescription
childrenReactNoderequiredOne element, used as the trigger

PopoverContent

PropTypeDefaultDescription
childrenReactNode-What the panel shows
...restHTMLAttributes<HTMLDivElement>-Spread onto the panel

Examples

Placement

<Popover placement="top" arrow>
  <PopoverTrigger>
    <Button variant="outlined">top</Button>
  </PopoverTrigger>

  <PopoverContent>
    <p>Above the trigger.</p>
  </PopoverContent>
</Popover>

placement takes any floating-ui placement — top, bottom-end, right-start and so on. The panel flips to the opposite side when there is no room, and shifts sideways to stay on screen.

offset is the gap between the trigger and the nearest part of the popover. With arrow on, that is measured to the arrow's tip rather than the panel edge, so the gap looks the same whether or not there is an arrow.

A form inside

import { Popover, PopoverTrigger, PopoverContent, usePopover } from '@geckoui/geckoui';

function FilterForm() {
  const { close } = usePopover();

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        close();
      }}
    >
      <Label htmlFor="min">Minimum</Label>
      <Input id="min" placeholder="0" />

      <Button type="button" variant="ghost" onClick={close}>Cancel</Button>
      <Button type="submit">Apply</Button>
    </form>
  );
}

<Popover>
  <PopoverTrigger>
    <Button>Filters</Button>
  </PopoverTrigger>

  <PopoverContent>
    <FilterForm />
  </PopoverContent>
</Popover>

usePopover gives anything inside the popover its state and a close, so a Cancel button or a form submit can shut it.

Escape closes the popover even from inside a text field, which is the one place a popover differs from Dialog and Drawer — they leave Escape alone while you are typing.

Controlled

const [open, setOpen] = useState(false);

<Button onClick={() => setOpen((prev) => !prev)}>Toggle from outside</Button>

<Popover open={open} onOpenChange={setOpen}>
  <PopoverTrigger>
    <Button>The trigger</Button>
  </PopoverTrigger>

  <PopoverContent>
    <p>Driven from outside.</p>
  </PopoverContent>
</Popover>

Focus

Focus moves into the panel when it opens — onto the first thing focusable inside, or the panel itself when there is nothing — and returns to the trigger when it closes.

Keyboard

KeyDoes
Enter SpaceOpen or close, from the trigger
EscapeClose and hand focus back to the trigger
TabMove through whatever is inside the panel

Accessibility

  • The trigger gets aria-expanded and aria-haspopup="dialog"
  • The panel is a dialog
  • Focus moves in on open and back to the trigger on close

Styling with CSS

.GeckoUIPopover                [the container, which wraps the trigger]
.GeckoUIPopover__content       [data-state="open|closed"]
.GeckoUIPopover__arrow

The trigger is your own element, so style it however you already do.

.GeckoUIPopover {
  --gecko-popover-bg: var(--color-surface-primary);
  --gecko-popover-border: var(--color-border-secondary);
  --gecko-popover-radius: 0.375rem;
  --gecko-popover-padding: 0.75rem;
  --gecko-popover-width: max-content;
  --gecko-popover-max-width: 20rem;
  --gecko-popover-z: 50;
}

The panel sizes itself to its content up to --gecko-popover-max-width. Set --gecko-popover-width to pin it to a fixed width instead.

Popover, Tooltip or Menu?

  • Tooltip — a short label on hover. Not focusable, not interactive
  • Popover — a panel you click open, holding anything, including a form
  • Menu — a list of actions, with the arrow keys moving between them
  • Tooltip - For a label rather than a panel
  • Menu - For a list of actions
  • Dialog - When it should take over the page