Popover

PreviousNext

A popover is a non-modal dialog that floats around a trigger. It is useful for displaying additional content without losing the context of the trigger element.

Component popover-basic not found in registry.

Component popover-close-button not found in registry.

Component popover-with-icon not found in registry.

Component popover-no-arrow not found in registry.

Component popover-positions not found in registry.

Installation

Install the required dependencies:

// @Diar Muradi Popover v0.0

"use client"

import * as React from "react"
import { tv, type VariantProps } from "@diarmuradi/ui/lib/index"
import {
  Dialog as AriaDialog,
  DialogProps as AriaDialogProps,
  DialogTrigger as AriaDialogTrigger,
  Popover as AriaPopover,
  PopoverProps as AriaPopoverProps,
  composeRenderProps,
  OverlayArrow,
} from "react-aria-components"

// Component identifier names
const POPOVER_CONTENT_NAME = "PopoverContent"
const POPOVER_DIALOG_NAME = "PopoverDialog"
const POPOVER_CLOSE_NAME = "PopoverClose"

export const popoverVariants = tv({
  slots: {
    content: [
      // base
      "w-max rounded-2xl bg-background p-5 shadow-xs ring-1 ring-inset ring-border",
      "z-50",
      // animation
      "data-[entering]:animate-in data-[exiting]:animate-out",
      "data-[exiting]:fade-out-0 data-[entering]:fade-in-0 data-[exiting]:zoom-out-95 data-[entering]:zoom-in-95",
      "data-[placement=bottom]:slide-in-from-top-2 data-[placement=left]:slide-in-from-right-2 data-[placement=right]:slide-in-from-left-2 data-[placement=top]:slide-in-from-bottom-2",
    ],
    arrow: ["group", "size-[11px]"],
    close: ["absolute right-4 top-4"],
  },
})

type PopoverSharedProps = VariantProps<typeof popoverVariants>

// Direct primitive assignments for components that don't need custom styling
const PopoverRoot = AriaDialogTrigger

// Content component
const PopoverContent = React.forwardRef<
  React.ComponentRef<typeof AriaPopover>,
  AriaPopoverProps & {
    showArrow?: boolean
    unstyled?: boolean
  }
>(
  (
    { children, className, offset = 12, showArrow = true, unstyled, ...rest },
    forwardedRef
  ) => {
    const { content, arrow } = popoverVariants()

    return (
      <AriaPopover
        ref={forwardedRef}
        offset={offset}
        className={composeRenderProps(className, (className) =>
          !unstyled ? content({ class: className }) : className || ""
        )}
        {...rest}
      >
        {(values) => (
          <>
            {showArrow && (
              <OverlayArrow className={arrow()}>
                {/* TODO: Fix the arrow */}
                <svg
                  width={12}
                  height={12}
                  viewBox="0 0 12 12"
                  className="group-placement-left:-rotate-90 fill-background stroke-border group-placement-bottom:rotate-180 group-placement-right:rotate-90 block"
                >
                  <path d="M0 0 L6 6 L12 0" />
                </svg>
              </OverlayArrow>
            )}
            {typeof children === "function" ? children(values) : children}
          </>
        )}
      </AriaPopover>
    )
  }
)
PopoverContent.displayName = POPOVER_CONTENT_NAME

// Dialog component for popover content
const PopoverDialog = React.forwardRef<
  React.ComponentRef<typeof AriaDialog>,
  AriaDialogProps & {
    unstyled?: boolean
  }
>(({ className, unstyled, ...rest }, forwardedRef) => {
  return (
    <AriaDialog
      ref={forwardedRef}
      className={!unstyled ? "p-4 outline-0" : className}
      {...rest}
    />
  )
})
PopoverDialog.displayName = POPOVER_DIALOG_NAME

// Close component
const PopoverClose = React.forwardRef<
  HTMLButtonElement,
  React.ComponentPropsWithoutRef<"button"> & {
    unstyled?: boolean
  }
>(({ className, unstyled, ...rest }, forwardedRef) => {
  const { close } = popoverVariants()

  return (
    <button
      ref={forwardedRef}
      className={!unstyled ? close({ class: className }) : className}
      {...rest}
    />
  )
})
PopoverClose.displayName = POPOVER_CLOSE_NAME

export type { PopoverSharedProps }
export {
  PopoverRoot as Root,
  PopoverContent as Content,
  PopoverDialog as Dialog,
  PopoverClose as Close,
}

Usage

import * as Popover from "@/registry/react-aria/ui/popover"
 
export function PopoverExample() {
  return (
    <Popover.Root>
      <Button.Root>Open Popover</Button.Root>
      <Popover.Content>
        <Popover.Dialog>
          <div className="space-y-2">
            <h4 className="font-medium">Popover Title</h4>
            <p className="text-muted-foreground text-sm">
              This is the popover content.
            </p>
          </div>
        </Popover.Dialog>
      </Popover.Content>
    </Popover.Root>
  )
}

Features

  • Accessible: Built on React Aria Components with full keyboard and screen reader support
  • Positioning: Smart positioning with collision detection and automatic repositioning
  • Animations: Smooth enter/exit animations with proper timing
  • Customizable: Full control over styling and behavior
  • Arrow Support: Optional arrow pointing to the trigger element
  • Close Button: Built-in close button support

Variants

Basic Popover

A simple popover with content.

<Popover.Root>
  <Button.Root>Open Popover</Button.Root>
  <Popover.Content>
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Basic Popover</h4>
        <p className="text-muted-foreground text-sm">
          This is a basic popover example.
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>

Popover with Close Button

A popover with a close button in the top right corner.

<Popover.Root>
  <Button.Root>With Close Button</Button.Root>
  <Popover.Content>
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Popover with Close</h4>
        <p className="text-muted-foreground text-sm">
          This popover has a close button.
        </p>
      </div>
      <Popover.Close>
        <Button.Root variant="secondary" size="sm" className="h-6 w-6 p-0">
          ×
        </Button.Root>
      </Popover.Close>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>

Popover without Arrow

A popover without the pointing arrow.

<Popover.Root>
  <Button.Root>No Arrow</Button.Root>
  <Popover.Content showArrow={false}>
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">No Arrow</h4>
        <p className="text-muted-foreground text-sm">
          This popover doesn't have an arrow.
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>

Positions

Basic Positions

Popovers can be positioned on different sides of the trigger element.

// Top position
<Popover.Root>
  <Button.Root>Top</Button.Root>
  <Popover.Content placement="top">
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Top Position</h4>
        <p className="text-muted-foreground text-sm">
          This popover appears above the trigger.
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>
 
// Bottom position
<Popover.Root>
  <Button.Root>Bottom</Button.Root>
  <Popover.Content placement="bottom">
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Bottom Position</h4>
        <p className="text-muted-foreground text-sm">
          This popover appears below the trigger.
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>
 
// Left position
<Popover.Root>
  <Button.Root>Left</Button.Root>
  <Popover.Content placement="left">
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Left Position</h4>
        <p className="text-muted-foreground text-sm">
          This popover appears to the left of the trigger.
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>
 
// Right position
<Popover.Root>
  <Button.Root>Right</Button.Root>
  <Popover.Content placement="right">
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Right Position</h4>
        <p className="text-muted-foreground text-sm">
          This popover appears to the right of the trigger.
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>

Alignment Options

You can also control the alignment of the popover relative to the trigger.

// Start alignment
<Popover.Root>
  <Button.Root>Top Start</Button.Root>
  <Popover.Content placement="top start">
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Top Start</h4>
        <p className="text-muted-foreground text-sm">
          Aligned to the start of the trigger.
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>
 
// End alignment
<Popover.Root>
  <Button.Root>Top End</Button.Root>
  <Popover.Content placement="top end">
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Top End</h4>
        <p className="text-muted-foreground text-sm">
          Aligned to the end of the trigger.
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>
 
// Center alignment (default)
<Popover.Root>
  <Button.Root>Top Center</Button.Root>
  <Popover.Content placement="top">
    <Popover.Dialog>
      <div className="space-y-2">
        <h4 className="font-medium">Top Center</h4>
        <p className="text-muted-foreground text-sm">
          Centered on the trigger (default).
        </p>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>

Examples

Settings Menu

A popover with a settings menu layout.

<Popover.Root>
  <Button.Root>
    <Settings className="mr-2 h-4 w-4" />
    Settings
  </Button.Root>
  <Popover.Content>
    <Popover.Dialog>
      <div className="space-y-3">
        <div className="flex items-center gap-2">
          <Settings className="text-muted-foreground h-4 w-4" />
          <h4 className="font-medium">Settings</h4>
        </div>
        <div className="space-y-2">
          <div className="flex items-center gap-2 text-sm">
            <User className="h-3 w-3" />
            <span>Profile Settings</span>
          </div>
          <div className="flex items-center gap-2 text-sm">
            <Calendar className="h-3 w-3" />
            <span>Calendar Settings</span>
          </div>
        </div>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>

Form in Popover

A popover containing a form.

<Popover.Root>
  <Button.Root>Add Item</Button.Root>
  <Popover.Content>
    <Popover.Dialog>
      <div className="space-y-4">
        <h4 className="font-medium">Add New Item</h4>
        <div className="space-y-3">
          <input
            type="text"
            placeholder="Item name"
            className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm"
          />
          <textarea
            placeholder="Description"
            className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm"
            rows={3}
          />
          <div className="flex justify-end gap-2">
            <Button.Root variant="secondary" size="sm">
              Cancel
            </Button.Root>
            <Button.Root size="sm">Add Item</Button.Root>
          </div>
        </div>
      </div>
    </Popover.Dialog>
  </Popover.Content>
</Popover.Root>

Accessibility

The Popover component is built on React Aria Components and includes:

  • Keyboard Navigation: Full keyboard support with Escape to close
  • Focus Management: Automatic focus trapping and restoration
  • Screen Reader Support: Proper ARIA attributes and announcements
  • Portal Rendering: Renders outside the DOM hierarchy to avoid z-index issues
  • Collision Detection: Smart positioning to keep content visible