Fiber UI LogoFiberUI

Popover

Displays rich content in a portal, triggered by a button.

Basic Usage

import { Button } from "@repo/ui/components/button";
import { Input } from "@repo/ui/components/input";
import { Label } from "@repo/ui/components/label";
import {
    Popover,
    PopoverBody,
    PopoverContent,
    PopoverTrigger,
} from "@repo/ui/components/popover";

export const Example1: React.FC = () => {
    return (
        <Popover>
            <PopoverTrigger>
                <Button>Open popover</Button>
            </PopoverTrigger>
            <PopoverContent className="w-80">
                <PopoverBody>
                    <div className="grid gap-4">
                        <div className="space-y-2">
                            <h4 className="font-medium leading-none">
                                Dimensions
                            </h4>
                            <p className="text-muted-foreground text-sm">
                                Set the dimensions for the layer.
                            </p>
                        </div>
                        <div className="grid gap-2">
                            <div className="grid grid-cols-3 items-center gap-4">
                                <Label htmlFor="width">Width</Label>
                                <Input
                                    id="width"
                                    defaultValue="100%"
                                    className="col-span-2 h-8"
                                />
                            </div>
                            <div className="grid grid-cols-3 items-center gap-4">
                                <Label htmlFor="maxWidth">Max. width</Label>
                                <Input
                                    id="maxWidth"
                                    defaultValue="300px"
                                    className="col-span-2 h-8"
                                />
                            </div>
                            <div className="grid grid-cols-3 items-center gap-4">
                                <Label htmlFor="height">Height</Label>
                                <Input
                                    id="height"
                                    defaultValue="25px"
                                    className="col-span-2 h-8"
                                />
                            </div>
                            <div className="grid grid-cols-3 items-center gap-4">
                                <Label htmlFor="maxHeight">Max. height</Label>
                                <Input
                                    id="maxHeight"
                                    defaultValue="none"
                                    className="col-span-2 h-8"
                                />
                            </div>
                        </div>
                    </div>
                </PopoverBody>
            </PopoverContent>
        </Popover>
    );
};

Directions of popover

"use client";

import { Button } from "@repo/ui/components/button";
import {
    Popover,
    PopoverBody,
    PopoverContent,
    PopoverTrigger,
} from "@repo/ui/components/popover";

export const Example2: React.FC = () => {
    return (
        <div className="grid grid-cols-2 gap-5">
            <Popover>
                <PopoverTrigger>
                    <Button>Top</Button>
                </PopoverTrigger>
                <PopoverContent showArrow placement="top">
                    <PopoverBody>This is TOP popover content</PopoverBody>
                </PopoverContent>
            </Popover>

            <Popover>
                <PopoverTrigger>
                    <Button>Right</Button>
                </PopoverTrigger>
                <PopoverContent showArrow placement="right">
                    <PopoverBody>This is RIGHT popover content</PopoverBody>
                </PopoverContent>
            </Popover>

            <Popover>
                <PopoverTrigger>
                    <Button>Left</Button>
                </PopoverTrigger>
                <PopoverContent showArrow placement="left">
                    <PopoverBody>This is LEFT popover content</PopoverBody>
                </PopoverContent>
            </Popover>

            <Popover>
                <PopoverTrigger>
                    <Button>Bottom</Button>
                </PopoverTrigger>
                <PopoverContent showArrow placement="bottom">
                    <PopoverBody>This is BOTTOM popover content</PopoverBody>
                </PopoverContent>
            </Popover>
        </div>
    );
};

All Components

import { Button } from "@repo/ui/components/button";
import { Checkbox } from "@repo/ui/components/checkbox";
import {
    Popover,
    PopoverTrigger,
    PopoverContent,
    PopoverHeader,
    PopoverBody,
    PopoverFooter,
} from "@repo/ui/components/popover";

export const Example3: React.FC = () => {
    return (
        <div className="flex min-h-[400px] items-center justify-center">
            <Popover>
                <PopoverTrigger>
                    <Button>Open Popover</Button>
                </PopoverTrigger>
                <PopoverContent className="w-80">
                    <PopoverHeader>
                        <h3 className="text-lg font-semibold">
                            Account Settings
                        </h3>
                    </PopoverHeader>
                    <PopoverBody>
                        <p className="mb-4 text-sm text-gray-600">
                            Manage your account preferences and settings here.
                            Changes will be saved automatically.
                        </p>
                        <div className="space-y-2">
                            <div className="flex items-center justify-between">
                                <span className="text-sm">
                                    Email notifications
                                </span>
                                <Checkbox defaultSelected />
                            </div>
                            <div className="flex items-center justify-between">
                                <span className="text-sm">
                                    Marketing emails
                                </span>
                                <Checkbox />
                            </div>
                        </div>
                    </PopoverBody>
                    <PopoverFooter>
                        <div className="flex justify-end gap-2">
                            <Button variant="ghost" size="sm">
                                Cancel
                            </Button>
                            <Button size="sm">Save Changes</Button>
                        </div>
                    </PopoverFooter>
                </PopoverContent>
            </Popover>
        </div>
    );
};

Component Code

"use client";
import {
    OverlayArrow,
    Popover as AriaPopover,
    PopoverProps as AriaPopoverProps,
    DialogTrigger,
    DialogTriggerProps,
    composeRenderProps,
    PopoverContext,
    useSlottedContext,
    Dialog,
} from "react-aria-components";
import React from "react";
import { tv, cn } from "tailwind-variants";

const popoverStyles = tv({
    base: "bg-popover text-popover-foreground z-50 rounded-md border p-4 shadow-md outline-none",
    variants: {
        isEntering: {
            true: "animate-in fade-in-0 zoom-in-95 duration-200",
        },
        isExiting: {
            true: "animate-out fade-out-0 zoom-out-95 duration-150",
        },
    },
});

// ============================================================================
// Popover (Wrapper)
// ============================================================================

export const Popover: React.FC<DialogTriggerProps> = (props) => {
    return <DialogTrigger {...props} />;
};

// ============================================================================
// PopoverTrigger
// ============================================================================

export const PopoverTrigger: React.FC<React.PropsWithChildren> = ({
    children,
}) => {
    return children;
};

// ============================================================================
// PopoverContent
// ============================================================================

export interface PopoverContentProps
    extends Omit<AriaPopoverProps, "children"> {
    showArrow?: boolean;
    children: React.ReactNode;
}

export const PopoverContent: React.FC<PopoverContentProps> = ({
    children,
    showArrow,
    className,
    ...props
}) => {
    const popoverContext = useSlottedContext(PopoverContext);
    const isSubmenu = popoverContext?.trigger === "SubmenuTrigger";
    let offset = showArrow ? 12 : 8;
    offset = isSubmenu ? offset - 6 : offset;

    return (
        <AriaPopover
            data-slot="popover-content"
            offset={offset}
            {...props}
            className={composeRenderProps(
                className,
                (className, renderProps) =>
                    cn(popoverStyles({ ...renderProps }), className) || "",
            )}
        >
            {showArrow && (
                <OverlayArrow className="group">
                    <svg
                        width={12}
                        height={12}
                        viewBox="0 0 12 12"
                        className="fill-popover stroke-border block group-data-[placement^=bottom]:rotate-180 group-data-[placement^=left]:-rotate-90 group-data-[placement^=right]:rotate-90"
                    >
                        <path d="M0 0 L6 6 L12 0" />
                    </svg>
                </OverlayArrow>
            )}
            <Dialog className="outline-none">{children}</Dialog>
        </AriaPopover>
    );
};

// ============================================================================
// PopoverHeader
// ============================================================================

export interface PopoverHeaderProps extends React.ComponentProps<"div"> {}

export const PopoverHeader: React.FC<PopoverHeaderProps> = ({
    children,
    className,
}) => {
    return (
        <div className={cn("pb-2", className)}>
            <h3 className="text-sm font-semibold leading-none">{children}</h3>
        </div>
    );
};

// ============================================================================
// PopoverBody
// ============================================================================

export interface PopoverBodyProps extends React.ComponentProps<"div"> {}
export const PopoverBody: React.FC<PopoverBodyProps> = ({
    children,
    className,
}) => {
    return <div className={cn("py-2", className)}>{children}</div>;
};

// ============================================================================
// PopoverFooter
// ============================================================================

export interface PopoverFooterProps extends React.ComponentProps<"div"> {}

export const PopoverFooter: React.FC<PopoverFooterProps> = ({
    children,
    className,
}) => {
    return <div className={cn("border-t pt-2", className)}>{children}</div>;
};