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
flipandpreventOverflow. - 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-popperThe 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-popperpackage 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
| Feature | Prop | Use when |
|---|---|---|
| Placement | placement="bottom-start" | You need exact side/alignment control. Supports Popper placements including auto, top-start, right-end, and bottom. |
| Strategy | strategy="fixed" | The reference is inside transformed, sticky, or fixed layouts. |
| Offset | offset={[skid, distance]} | You need distance from the trigger or cross-axis shifting. |
| Flip | flip | The panel should move to another side when it would overflow. |
| Overflow prevention | preventOverflow | The panel must stay visible inside the viewport or clipping parent. |
| Arrow | arrow | The panel needs a directional pointer tied to the reference. |
| Hide | hideWhenReferenceHidden | The panel should hide when the trigger scrolls out of view. |
| Match width | matchReferenceWidth | The content width should equal the trigger width. |
| Custom modifiers | modifiers={[...]} | You need Popper lifecycle hooks, data attributes, custom transforms, or boundary tuning. |
| Virtual reference | referenceElement={virtualElement} | The anchor is a coordinate, range, cursor point, or canvas item instead of a DOM node. |
| Portal | portalled | The 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.bodytarget 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, andshadow-mdinstead 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. PopperContentexposessize="sm" | "md" | "lg" | "auto"and accepts Tailwind width classes for custom cases.PopperContentaccepts arbitrary React children, including async state, Skeletons, forms, and nested design-system components. It never renders raw HTML.- Use
arrowClassNamewhen a custom panel background needs the arrow to match.
Related
- 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.