rapidUI

Popover

Floating panel anchored to a trigger, built on Base UI Popover.

Installation

pnpm dlx shadcn@latest add https://rdf.dev.360online.com/r/popover.json

Installs to @/components/ui/popover.

Manual installation

Install the dependencies:

pnpm add @base-ui/react@^1.8.0

Copy the source into your project:

components/ui/popover.tsx
'use client'

import { Popover as PopoverPrimitive } from '@base-ui/react/popover'
import type * as React from 'react'
import { cn } from '@/lib/utils'

function Popover({ ...props }: PopoverPrimitive.Root.Props) {
	return <PopoverPrimitive.Root data-slot="popover" {...props} />
}

function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
	return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}

function PopoverContent({
	className,
	align = 'center',
	alignOffset = 0,
	side = 'bottom',
	sideOffset = 4,
	anchor,
	...props
}: PopoverPrimitive.Popup.Props &
	Pick<PopoverPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset' | 'anchor'>) {
	return (
		<PopoverPrimitive.Portal>
			<PopoverPrimitive.Positioner
				align={align}
				alignOffset={alignOffset}
				side={side}
				sideOffset={sideOffset}
				anchor={anchor}
				className="isolate z-50"
			>
				<PopoverPrimitive.Popup
					data-slot="popover-content"
					className={cn(
						'z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-border outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
						className,
					)}
					{...props}
				/>
			</PopoverPrimitive.Positioner>
		</PopoverPrimitive.Portal>
	)
}

function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
	return <div data-slot="popover-header" className={cn('flex flex-col gap-0.5 text-sm', className)} {...props} />
}

function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
	return <PopoverPrimitive.Title data-slot="popover-title" className={cn('font-medium', className)} {...props} />
}

function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
	return (
		<PopoverPrimitive.Description
			data-slot="popover-description"
			className={cn('text-muted-foreground', className)}
			{...props}
		/>
	)
}

export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger }

A popover component built on top of Base UI Popover, providing a floating element that displays additional content when triggered.

Features

  • Built on the Base UI Popover primitive
  • Custom styling with Tailwind CSS
  • Accessible by default
  • Multiple positioning options
  • Controlled and uncontrolled modes
  • Custom trigger support
  • Animated transitions
  • Header, title and description building blocks (PopoverHeader, PopoverTitle, PopoverDescription)
  • Focus management
  • Screen reader support

Usage

import {
  Popover,
  PopoverTrigger,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverDescription,
} from '@/components/ui/popover'
import { Button } from '@/components/ui/button'

// Basic usage
export function BasicPopover() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="outline" />}>Opening hours</PopoverTrigger>
      <PopoverContent className="w-72">
        <PopoverHeader>
          <PopoverTitle>Citizen Service Centre</PopoverTitle>
          <PopoverDescription>City Hall, Kungsgatan 3</PopoverDescription>
        </PopoverHeader>
      </PopoverContent>
    </Popover>
  )
}

// Controlled usage
export function ControlledPopover() {
  const [open, setOpen] = React.useState(false)

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger render={<Button variant="outline" />}>Add note</PopoverTrigger>
      <PopoverContent>
        {/* Form fields */}
        <Button onClick={() => setOpen(false)}>Save note</Button>
      </PopoverContent>
    </Popover>
  )
}

Examples

Default

Opening hours for the citizen service centre, with today in bold.

With Form

A controlled popover for adding an internal note to a case. Cancel and Save close it through state.

With Icon

An icon-only filter button with a checkbox list and a close button.

Contact Card

Contact details for the case officer, opened from an inline link.

Custom Positioning

Popovers with different side values.

Props

Popover

  • defaultOpen - The open state when initially rendered
  • open - The controlled open state
  • onOpenChange - Event handler for open state changes
  • modal - Whether the popover is modal

PopoverTrigger

  • render - Element to render instead of the default <button>, e.g. render={<Button variant="outline" />}
  • openOnHover - Also open the popover when the trigger is hovered
  • children - The trigger's content

PopoverContent

  • align - The alignment of the popover (default 'center')
  • alignOffset - Offset along the alignment axis (default 0)
  • side - The preferred side of the trigger to render (default 'bottom')
  • sideOffset - The distance from the trigger (default 4)
  • anchor - Element (or ref) to position against instead of the trigger. Replaces PopoverAnchor.
  • className - Additional CSS classes
  • children - Content to display in the popover

PopoverHeader

  • Stacks a PopoverTitle and PopoverDescription
  • className - Additional CSS classes

PopoverTitle

  • Renders an <h2> and labels the popover (aria-labelledby), so screen readers announce its name
  • render - Render a different heading level, e.g. render={<h3 />}
  • className - Additional CSS classes
  • children - Title text

PopoverDescription

  • Describes the popover (aria-describedby)
  • className - Additional CSS classes
  • children - Muted description text

PopoverClose and PopoverArrow are no longer exported. Close the popover with controlled open state (as in the With Form and With Icon examples) and style the content without an arrow.

Accessibility

The Popover component follows WAI-ARIA guidelines:

  • Uses proper ARIA roles and attributes
  • Manages focus trap within popover
  • Supports keyboard navigation:
    • Esc: Close popover
    • Tab: Navigate focusable elements
    • Shift+Tab: Navigate focusable elements (reverse)
  • Announces popover state changes
  • Provides clear action buttons
  • Maintains focus on trigger after closing

Best Practices

  1. Content:

    • Keep content concise and focused
    • Use clear headings
    • Group related information
    • Consider mobile viewports
  2. Positioning:

    • Choose appropriate placement
    • Handle overflow gracefully
    • Consider screen edges
    • Maintain visibility
  3. Interaction:

    • Provide clear triggers
    • Handle click outside
    • Consider touch devices
    • Manage focus properly
  4. Accessibility:

    • Write descriptive labels
    • Use semantic markup
    • Test with screen readers
    • Verify keyboard navigation
  5. Implementation:

    • Handle state changes
    • Consider animations
    • Test edge cases
  • Tooltip
  • Hover Card
  • Dialog
  • Dropdown Menu
  • HoverCard