MiHCM
overlay/@mihcm/ui·v0.21.0·stable

Popper

Low-level Popper.js v2 positioning primitive for custom floating panels, virtual anchors, arrows, portals, and modifiers.

Use cases

  • Custom floating UI — build advanced overlays when Tooltip, Popover, DropdownMenu, or Select do not expose enough positioning control.
  • Context menus from coordinates — position against a Popper virtual element instead of a DOM trigger.
  • Collision-aware panels — keep panels inside scroll containers and the viewport with flip and preventOverflow.
  • Reference-width panels — align command palettes, search panels, and rich selects to their trigger width.

Install

Popper uses the official Popper v2 React integration:

pnpm add @popperjs/core react-popper

The design-system package already includes these dependencies. Application teams importing from @mihcm/ui/Popper only need the normal design-system install.

React 19 note: the wrapper is validated in this workspace on React 19. The upstream react-popper package may still publish an older React peer range until its metadata is updated.

Basic usage

import { Popper, PopperTrigger, PopperContent } from '@mihcm/ui/Popper';
import { Button } from '@mihcm/ui/Button';
 
<Popper placement="bottom-start" offset={[0, 8]} arrow>
  <PopperTrigger>
    <Button variant="outline">Open panel</Button>
  </PopperTrigger>
  <PopperContent>
    <div className="space-y-2">
      <p className="text-sm font-medium">Payroll cutoff</p>
      <p className="text-sm text-muted-foreground">Review the current workflow state.</p>
    </div>
  </PopperContent>
</Popper>

Feature map

FeaturePropUse when
Placementplacement="bottom-start"You need exact side/alignment control. Supports Popper placements including auto, top-start, right-end, and bottom.
Strategystrategy="fixed"The reference is inside transformed, sticky, or fixed layouts.
Offsetoffset={[skid, distance]}You need distance from the trigger or cross-axis shifting.
FlipflipThe panel should move to another side when it would overflow.
Overflow preventionpreventOverflowThe panel must stay visible inside the viewport or clipping parent.
ArrowarrowThe panel needs a directional pointer tied to the reference.
HidehideWhenReferenceHiddenThe panel should hide when the trigger scrolls out of view.
Match widthmatchReferenceWidthThe content width should equal the trigger width.
Custom modifiersmodifiers={[...]}You need Popper lifecycle hooks, data attributes, custom transforms, or boundary tuning.
Virtual referencereferenceElement={virtualElement}The anchor is a coordinate, range, cursor point, or canvas item instead of a DOM node.
PortalportalledThe layer should render in document.body to avoid clipping and stacking issues.

React Portal behavior

PopperContent uses React DOM createPortal when portalled is enabled. A portal moves the rendered DOM node to another DOM container, but it does not move the component out of the React tree. Context still works, and events bubble through the React parent tree.

<Popper portalled>
  <PopperTrigger>
    <Button variant="outline">Open panel</Button>
  </PopperTrigger>
  <PopperContent>
    <p className="text-sm">This content renders in document.body.</p>
  </PopperContent>
</Popper>

By default, Popper portals to document.body. Keep this default for overlays that may appear inside clipped, transformed, sticky, or scrollable containers.

Disable the portal only when the layer must remain inside a local stacking context:

<div className="relative overflow-hidden rounded-lg border border-border">
  <Popper portalled={false} placement="bottom-start">
    <PopperTrigger>
      <Button variant="outline">Contained panel</Button>
    </PopperTrigger>
    <PopperContent>
      <p className="text-sm">This content stays inside the local DOM subtree.</p>
    </PopperContent>
  </Popper>
</div>

Portal checklist:

  • The portal target must exist before rendering. The built-in document.body target is guarded for client rendering.
  • Events from portal children bubble through the React tree, not just the DOM tree. Stop propagation inside the portal only when a parent handler would close or mutate the layer incorrectly.
  • Portals do not solve accessibility by themselves. Manage focus, labels, roles, Escape behavior, and return focus for the widget pattern you are building.
  • Portals can affect stacking. Prefer semantic layer classes such as z-50, bg-popover, border-border, and shadow-md instead of raw CSS values.

Controlled and uncontrolled

Use uncontrolled state for simple overlays:

<Popper defaultOpen>
  <PopperTrigger>
    <Button>Open</Button>
  </PopperTrigger>
  <PopperContent>Content</PopperContent>
</Popper>

Use controlled state when another component owns the layer lifecycle:

const [open, setOpen] = useState(false);
 
<Popper open={open} onOpenChange={setOpen}>
  ...
</Popper>

Virtual elements

Popper supports anchors without DOM nodes. Provide an object with getBoundingClientRect().

const virtualElement = {
  getBoundingClientRect: () => ({
    width: 0,
    height: 0,
    x,
    y,
    top: y,
    right: x,
    bottom: y,
    left: x,
  } as DOMRect),
};
 
<Popper referenceElement={virtualElement} open={open} strategy="fixed">
  <PopperContent>Context actions</PopperContent>
</Popper>

Custom modifiers

Pass Popper modifiers for behavior the design-system wrapper does not own directly.

const modifiers = [
  {
    name: 'addDataAttribute',
    enabled: true,
    phase: 'write',
    fn({ state }) {
      state.elements.popper.setAttribute('data-layer', 'employee-actions');
    },
  },
];
 
<Popper modifiers={modifiers}>
  ...
</Popper>

Styling contract

  • Use Tailwind classes and design-system tokens through className.
  • Do not hardcode colors in component implementations. Use bg-popover, text-popover-foreground, border-border, shadow-*, and semantic state classes.
  • PopperContent exposes size="sm" | "md" | "lg" | "auto" and accepts Tailwind width classes for custom cases.
  • PopperContent accepts arbitrary React children, including async state, Skeletons, forms, and nested design-system components. It never renders raw HTML.
  • Use arrowClassName when a custom panel background needs the arrow to match.
  • Tooltip — non-interactive hover/focus hints.
  • Popover — easier click-triggered floating panel.
  • DropdownMenu — action menu with managed keyboard behavior.
  • Select — selection controls backed by Radix or React Select.