AniUI

Popover

A popover that displays rich content in a floating panel triggered by a button press.

Web preview — components render natively on iOS & Android
import { Text } from "react-native";
import {
  Popover,
  PopoverTrigger,
  PopoverContent,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";

export function MyScreen() {
  return (
    <Popover>
      <PopoverTrigger>
        <Button>Open Popover</Button>
      </PopoverTrigger>
      <PopoverContent>
        <Text className="text-sm text-card-foreground">
          This is the popover content. Place anything here.
        </Text>
      </PopoverContent>
    </Popover>
  );
}

Installation#

npx @aniui/cli add popover

This component requires @rn-primitives/popover, @rn-primitives/portal, and react-native-reanimated.

Usage#

app/index.tsx
import { Text } from "react-native";
import {
  Popover,
  PopoverTrigger,
  PopoverContent,
} from "@/components/ui/popover";
import { Button } from "@/components/ui/button";

export function MyScreen() {
  return (
    <Popover>
      <PopoverTrigger>
        <Button>Open Popover</Button>
      </PopoverTrigger>
      <PopoverContent>
        <Text className="text-sm text-card-foreground">
          This is the popover content. Place anything here.
        </Text>
      </PopoverContent>
    </Popover>
  );
}

Components#

Popover is a compound component made up of several parts:

ComponentDescription
Popover

Root component that manages open/closed state. Supports both controlled and uncontrolled usage.

PopoverTrigger

The pressable element that toggles the popover.

PopoverContent

The floating panel that displays the popover content. Supports side, sideOffset, and align props.

PopoverClose

Pressable element that closes the popover.

Props#

Popover#

PropTypeDefault
open
boolean
onOpenChange
(open: boolean) => void
children
React.ReactNode
required

PopoverContent#

PropTypeDefault
side
"top" | "bottom" | "left" | "right"
"bottom"
sideOffset
number
8
align
"start" | "center" | "end"
"center"
className
string

All sub-components also accept className and their respective React Native base props.

Accessibility#

  • Uses @rn-primitives/popover for proper trigger-relative positioning
  • Collision detection prevents overflow off screen edges
  • BackHandler dismisses on Android
  • accessibilityRole="button" on trigger
  • Requires <PortalHost /> at app root

Source#

components/ui/popover.tsx
import React from "react";
import { View, Pressable } from "react-native";
import * as PopoverPrimitive from "@rn-primitives/popover";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { cn } from "@/lib/utils";

export interface PopoverProps {
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  children: React.ReactNode;
}

export function Popover({ open, onOpenChange, children }: PopoverProps) {
  return <PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>{children}</PopoverPrimitive.Root>;
}

export interface PopoverTriggerProps extends React.ComponentPropsWithoutRef<typeof Pressable> {
  className?: string;
  children?: React.ReactNode;
}

export function PopoverTrigger({ className, children, ...props }: PopoverTriggerProps) {
  return (
    <PopoverPrimitive.Trigger asChild>
      <Pressable className={cn("min-h-12 min-w-12", className)} accessible={true} accessibilityRole="button" {...props}>
        {children}
      </Pressable>
    </PopoverPrimitive.Trigger>
  );
}

export interface PopoverContentProps extends React.ComponentPropsWithoutRef<typeof View> {
  className?: string;
  children?: React.ReactNode;
  side?: "top" | "bottom" | "left" | "right";
  sideOffset?: number;
  align?: "start" | "center" | "end";
}

export function PopoverContent({ className, children, side = "bottom", sideOffset = 8, align = "center", ...props }: PopoverContentProps) {
  return (
    <PopoverPrimitive.Portal>
      <PopoverPrimitive.Overlay className="absolute inset-0" />
      <PopoverPrimitive.Content side={side} sideOffset={sideOffset} align={align} avoidCollisions>
        <Animated.View entering={FadeIn.duration(150)} exiting={FadeOut.duration(100)}>
          <View className={cn("w-72 rounded-lg border border-border bg-card p-4 shadow-lg", className)} {...props}>
            {children}
          </View>
        </Animated.View>
      </PopoverPrimitive.Content>
    </PopoverPrimitive.Portal>
  );
}

export function PopoverClose({ children, className, ...props }: React.ComponentPropsWithoutRef<typeof Pressable> & { className?: string; children?: React.ReactNode }) {
  return (
    <PopoverPrimitive.Close asChild>
      <Pressable className={cn("", className)} accessible={true} accessibilityRole="button" {...props}>
        {children}
      </Pressable>
    </PopoverPrimitive.Close>
  );
}