Components
Overlay & Dialogs
Popover

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 triggerAsChild when trigger is a custom button/link component.
  • Use autoHeight for short content and set height for long/scrollable content.
  • Use preventAutoFocus if input focus should remain unchanged when opening.
  • Use preventInputClose when users interact with input fields inside or around the popover.

API Reference

Props

PropTypeDefaultDescription
triggerReactNodeElement that opens the popover
triggerAsChildbooleanfalseRender trigger as child (recommended for Button)
openbooleanControlled open state
onOpenChange(open: boolean) => voidCallback when open state changes
defaultOpenbooleanfalseInitial open state in uncontrolled mode
modalbooleanfalseWhether popover behaves as modal
showHeaderbooleanfalseRender header section
titlestringHeader title
descriptionstringHeader description
showFooterbooleanfalseRender footer section
footerContentReactNodeContent rendered in footer
side"top" | "bottom" | "left" | "right""bottom"Side placement
align"start" | "center" | "end""center"Alignment on selected side
sideOffsetnumber4Distance from trigger
alignOffsetnumber0Alignment offset
autoHeightbooleanfalseFit content height automatically
heightstring"350px"Height when autoHeight is false
maxWidthstring"90vw"Max width and width of content
preventAutoFocusbooleanfalsePrevent focus changes on open/close
preventInputClosebooleanfalseKeep popover open when interacting with inputs
childrenReactNodeMain popover content

MIT 2026 © @hoag/ui