Popup

A floating surface for option lists (Listbox) and small panels, such as a filter form. For a short description on hover, use a Tooltip.

Installation

bun add @harmo-ui/css @harmo-ui/react

Usage

import { useId } from "react";
import { Button, Field, FilterChip, Input, Popup } from "@harmo-ui/react";

const id = useId();

<Popup id={id} trigger={<FilterChip label="Name contains" />}>
  <div style={{ display: "grid", gap: 16, padding: 12 }}>
    <Field label="Name contains">
      <Input name="contains" type="text" />
    </Field>
    <Button popoverTarget={id} popoverTargetAction="hide">
      Apply
    </Button>
  </div>
</Popup>

Examples

This site is using your theme

Panel

A Filter chip opening a text filter. The trigger's popovertarget opens and closes the popup without a script, and an outside click or Escape closes it too. anchor-name and position-anchor tie the two together. The popup has no padding: lay out its content yourself, as the style does here. A button inside with popovertargetaction="hide" closes it.

const id = useId();
const popup = useRef<HTMLDivElement>(null);
const input = useRef<HTMLInputElement>(null);
const [text, setText] = useState("");
const apply = () => {
  setText(input.current?.value.trim() ?? "");
  popup.current?.hidePopover();
};

return (
  <Popup
    ref={popup}
    id={id}
    trigger={
      <FilterChip label="Name contains" values={text ? [text] : undefined} />
    }
  >
    <div style={{ display: "grid", gap: 16, padding: 12 }}>
      <Field label="Name contains">
        <Input
          ref={input}
          name="contains"
          type="text"
          onKeyDown={(event) => {
            if (event.key !== "Enter") return;
            event.preventDefault();
            apply();
          }}
        />
      </Field>
      <Button
        popoverTarget={id}
        popoverTargetAction="hide"
        onClick={apply}
      >
        Apply
      </Button>
    </div>
  </Popup>
);

Placement

8px below its trigger, aligned to its start, and at least as wide. Without room below it opens above, sliding up instead of down; without room at the end it aligns to the trigger's other edge. Scroll until this button is near the bottom of the window to see it open above.

  • Last 7 days
  • Last 30 days
  • Last 90 days
<Popup
  trigger={
    <Button variant="secondary" aria-haspopup="listbox">
      Last 30 days
      <ExpandChevron />
    </Button>
  }
>
  <Listbox aria-label="Period" defaultValue="30">
    <Option value="7">Last 7 days</Option>
    <Option value="30">Last 30 days</Option>
    <Option value="90">Last 90 days</Option>
  </Listbox>
</Popup>

API reference

React

<Popup> renders its trigger, then the popup holding its children, and connects them: the trigger gets popovertarget, aria-expanded, and an anchor name, and the popup's data-side follows the side it opens on, so it slides away from the trigger. Other props, ref included, are the popup <div>'s.

PropEffect
triggerThe element that opens it, such as a <Button> or a <FilterChip>
idThe popup's id; generated when left out. Pass your own to close it from a button inside, with popoverTarget and popoverTargetAction="hide"

HTML

A <div class="hpopup" popover>, opened by a trigger with popovertarget. The trigger's style="anchor-name: --…" matches the popup's style="position-anchor: --…". The popup has no padding, so a panel lays out its own content. Load popup.css.

HookEffect
data-side="top"The popup opened above its trigger, so it slides from below

It needs a script for aria-expanded and data-side. See Popup's full contract.

Accessibility

  • The browser closes it on outside click and Escape; a popup opened from inside another keeps the outer one open.
  • Hidden content can't be focused or reached by screen readers.
  • aria-expanded on the trigger tells screen readers whether it's open.