Popover Wrapper
PopoverWrapper is a convenience component built on top of Radix UI's Popover. It provides a structured layout with optional header and footer sections, plus built-in sizing and positioning controls. This makes it a good fit for filters, quick actions, and small contextual forms.
Installation
1. Ensure dependencies are available:
@hoag/ui/popover-wrapper@hoag/ui/legacy/popover(primitive under the wrapper)
2. Copy wrapper source if needed:
- Source in this monorepo:
packages/ui/src/common/overlays/popover-wrapper.tsx - Place in your app:
components/common/overlays/popover-wrapper.tsx
3. Update import paths to match your project structure.
Usage
import { PopoverWrapper } from "@hoag/ui/popover-wrapper";
import { Button } from "@hoag/ui/components/button";
export default function Demo() {
return (
<PopoverWrapper
trigger={<Button variant="outline">Open</Button>}
triggerAsChild
showHeader
title="Filter options"
autoHeight
maxWidth="24rem"
>
<div>Popover content</div>
</PopoverWrapper>
);
}Default
<PopoverWrapper
trigger={<Button variant="outline">Open Popover</Button>}
triggerAsChild
autoHeight
maxWidth="24rem"
showHeader
title="Popover Title"
description="Optional description text."
>
<div>Popover content goes here.</div>
</PopoverWrapper>Placement
<PopoverWrapper trigger={<Button>Top</Button>} title="Top" side="top">
<p>Opens from top.</p>
</PopoverWrapper>
<PopoverWrapper trigger={<Button>Right</Button>} title="Right" side="right">
<p>Opens from right.</p>
</PopoverWrapper>With Footer Actions
<PopoverWrapper
trigger={<Button>Open Actions</Button>}
triggerAsChild
showHeader
showFooter
title="Quick Actions"
description="Perform actions without leaving the page."
autoHeight
maxWidth="24rem"
footerContent={
<div className="flex gap-2">
<Button size="sm">Apply</Button>
<Button size="sm" variant="outline">
Reset
</Button>
</div>
}
>
<div>Popover content</div>
</PopoverWrapper>Without Header
<PopoverWrapper
trigger={<Button variant="ghost">Minimal</Button>}
triggerAsChild
autoHeight
maxWidth="24rem"
>
<div>No title or description — clean content.</div>
</PopoverWrapper>Controlled Mode
Use controlled mode when open state depends on parent logic (e.g. close after submit or sync with route/search state).
import { useState } from "react";
export default function ControlledPopover() {
const [open, setOpen] = useState(false);
return (
<PopoverWrapper
open={open}
onOpenChange={setOpen}
trigger={<Button variant="outline">Toggle</Button>}
triggerAsChild
showHeader
title="Controlled popover"
autoHeight
maxWidth="24rem"
>
<div className="space-y-3 py-2">
<p className="text-base text-muted-foreground">
State is managed by parent.
</p>
<Button size="sm" onClick={() => setOpen(false)}>
Close
</Button>
</div>
</PopoverWrapper>
);
}Tips
- Use
triggerAsChildwhentriggeris a custom button/link component. - Use
autoHeightfor short content and setheightfor long/scrollable content. - Use
preventAutoFocusif input focus should remain unchanged when opening. - Use
preventInputClosewhen users interact with input fields inside or around the popover.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
trigger | ReactNode | — | Element that opens the popover |
triggerAsChild | boolean | false | Render trigger as child (recommended for Button) |
open | boolean | — | Controlled open state |
onOpenChange | (open: boolean) => void | — | Callback when open state changes |
defaultOpen | boolean | false | Initial open state in uncontrolled mode |
modal | boolean | false | Whether popover behaves as modal |
showHeader | boolean | false | Render header section |
title | string | — | Header title |
description | string | — | Header description |
showFooter | boolean | false | Render footer section |
footerContent | ReactNode | — | Content rendered in footer |
side | "top" | "bottom" | "left" | "right" | "bottom" | Side placement |
align | "start" | "center" | "end" | "center" | Alignment on selected side |
sideOffset | number | 4 | Distance from trigger |
alignOffset | number | 0 | Alignment offset |
autoHeight | boolean | false | Fit content height automatically |
height | string | "350px" | Height when autoHeight is false |
maxWidth | string | "90vw" | Max width and width of content |
preventAutoFocus | boolean | false | Prevent focus changes on open/close |
preventInputClose | boolean | false | Keep popover open when interacting with inputs |
children | ReactNode | — | Main popover content |