# Popover

An accessible React popover for Next.js and Tailwind CSS: forms, settings and other rich content anchored to a button, built on Radix Popover.

Source: https://ui.fewtone.com/components/popover

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/popover.json
```

## Usage

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

export function Dimensions() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Dimensions</Button>
      </PopoverTrigger>
      <PopoverContent>
        <PopoverHeader>
          <PopoverTitle>Dimensions</PopoverTitle>
          <PopoverDescription>Set the size of the layer.</PopoverDescription>
        </PopoverHeader>
      </PopoverContent>
    </Popover>
  )
}
```

## API reference

### Popover

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – | Whether it's open, when you control it. |
| `defaultOpen` | `boolean` | `false` | Whether it starts open. |
| `onOpenChange` | `(open: boolean) => void` | – | Called when it opens or closes. |
| `modal` | `boolean` | `false` | Keeps focus inside and makes the rest of the page inert while it's open, like a dialog. |

### PopoverTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the single child (for example a Button) as the trigger. |

### PopoverAnchor

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the single child as the anchor. |

### PopoverContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | The side of the trigger it opens on. It flips when there's no room. |
| `align` | `"start" \| "center" \| "end"` | `"center"` | How it lines up with the trigger along that side. |
| `sideOffset` | `number` | `4` | Gap between the trigger and the popover, in px. |
| `onOpenAutoFocus` | `(event: Event) => void` | – | Called when focus moves in on open. Call event.preventDefault() to keep it on the trigger. |

### PopoverHeader

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### PopoverTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### PopoverDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |

### PopoverClose

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Render the single child (for example a Button) as the close button. |

**When to use it:** Use Popover for interactive content anchored to a button: a small form, settings, filters or a share link. Use Tooltip for a short text hint, Hover Card for a preview on hover, and Select to pick one value from a list.
**Built on:** Radix Popover.
**Accessibility pattern:** https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/
**npm packages (installed by the CLI):** motion@^14.0.0, radix-ui@^1.7.0.

## Examples

### Dimensions and Share

Two everyday popovers. In Dimensions, turn on Limit size: two fields come in and the popover glides to its new height instead of jumping. Focus moves into the popover when it opens, and Escape closes it.

```tsx
"use client";

import { Button } from "@/components/ui/button";
import { CopyButton } from "@/components/ui/copy-button";
import { Input } from "@/components/ui/input";
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
import { useId, useState } from "react";

const size = [
  { key: "width", label: "Width", value: "100%" },
  { key: "height", label: "Height", value: "25px" },
];

const limits = [
  { key: "max-width", label: "Max. width", value: "300px" },
  { key: "max-height", label: "Max. height", value: "none" },
];

const link = "https://ui.fewtone.com/components/popover";

function Field({ id, label, value }: { id: string; label: string; value: string }) {
  return (
    <div className="grid grid-cols-3 items-center gap-4">
      <label htmlFor={id} className="text-sm">
        {label}
      </label>
      <Input id={id} defaultValue={value} size="sm" className="col-span-2" />
    </div>
  );
}

/** Turning on Limit size adds two fields, and the popover glides to its new height */
function Dimensions() {
  const id = useId();
  const [limit, setLimit] = useState(false);

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Dimensions</Button>
      </PopoverTrigger>
      <PopoverContent className="grid w-80 gap-4">
        <PopoverHeader>
          <PopoverTitle>Dimensions</PopoverTitle>
          <PopoverDescription>Set the size of the layer.</PopoverDescription>
        </PopoverHeader>
        <div className="grid gap-2">
          {size.map((field) => (
            <Field key={field.key} id={`${id}-${field.key}`} label={field.label} value={field.value} />
          ))}
          <div className="grid h-(--size-control-sm) grid-cols-3 items-center gap-4">
            <label htmlFor={`${id}-limit`} className="text-sm">
              Limit size
            </label>
            <Switch id={`${id}-limit`} checked={limit} onCheckedChange={setLimit} />
          </div>
          {limit ? (
            // The new fields come in through the blur while the popover glides open around them
            <div className="grid animate-popover-in gap-2">
              {limits.map((field) => (
                <Field key={field.key} id={`${id}-${field.key}`} label={field.label} value={field.value} />
              ))}
            </div>
          ) : null}
        </div>
      </PopoverContent>
    </Popover>
  );
}

function Share() {
  const id = useId();

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Share</Button>
      </PopoverTrigger>
      <PopoverContent className="grid w-80 gap-4">
        <PopoverHeader>
          <PopoverTitle>Share this page</PopoverTitle>
          <PopoverDescription>Anyone with the link can view it.</PopoverDescription>
        </PopoverHeader>
        <div className="flex gap-2">
          <label htmlFor={id} className="sr-only">
            Link
          </label>
          <Input id={id} value={link} readOnly size="sm" />
          <CopyButton value={link} variant="outline" size="icon-sm" aria-label="Copy link" />
        </div>
      </PopoverContent>
    </Popover>
  );
}

export default function PopoverUses() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      <Dimensions />
      <Share />
    </div>
  );
}
```

### Side and alignment

A popover opens on any side of its trigger and lines up with its start, centre or end. It grows out of the edge it's attached to, and flips when there's no room.

```tsx
"use client";

import { Button } from "@/components/ui/button";
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { useId, useState } from "react";

type Align = "start" | "center" | "end";

const sides = [
  { side: "top", label: "Top", opens: "Opens above" },
  { side: "right", label: "Right", opens: "Opens to the right" },
  { side: "bottom", label: "Bottom", opens: "Opens below" },
  { side: "left", label: "Left", opens: "Opens to the left" },
] as const;

const lineUp: Record<Align, string> = {
  start: "lined up with the start of its button",
  center: "centred on its button",
  end: "lined up with the end of its button",
};

/*
 * One popover per side, with the alignment in the top-right corner. Each one opens out of its button's
 * edge. An empty row at the bottom, as tall as the setting's row, keeps the buttons centred.
 */
export default function PopoverSides() {
  const id = useId();
  const [align, setAlign] = useState<Align>("center");

  return (
    <div className="flex w-full flex-col gap-6 self-stretch">
      <div className="-mt-6 -mr-6 flex items-center justify-end gap-2">
        <label htmlFor={`${id}-align`} className="text-sm text-muted-foreground">
          Align
        </label>
        <Select value={align} onValueChange={(value) => setAlign(value as Align)}>
          <SelectTrigger id={`${id}-align`} size="sm" className="w-28">
            <SelectValue />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="start">Start</SelectItem>
            <SelectItem value="center">Center</SelectItem>
            <SelectItem value="end">End</SelectItem>
          </SelectContent>
        </Select>
      </div>

      <div className="flex flex-1 flex-wrap items-center justify-center gap-3">
        {sides.map(({ side, label, opens }) => (
          <Popover key={side}>
            <PopoverTrigger asChild>
              <Button variant="outline">{label}</Button>
            </PopoverTrigger>
            <PopoverContent side={side} align={align} className="w-56">
              <PopoverHeader>
                <PopoverTitle>{label}</PopoverTitle>
                <PopoverDescription>
                  {opens}, {lineUp[align]}.
                </PopoverDescription>
              </PopoverHeader>
            </PopoverContent>
          </Popover>
        ))}
      </div>

      <div aria-hidden className="-mb-6 h-(--size-control-sm)" />
    </div>
  );
}
```

### Anchored to a field

With PopoverAnchor, the filter button opens the popover but it lines up with the whole search field and takes its width. PopoverClose closes it and focus goes back to the button.

```tsx
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import {
  Popover,
  PopoverAnchor,
  PopoverClose,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover";
import { SlidersHorizontalIcon } from "lucide-react";
import { useId } from "react";

const filters = [
  { key: "archived", label: "Include archived files", on: false },
  { key: "mine", label: "Only files I own", on: true },
  { key: "shared", label: "Shared with me", on: true },
];

/** The button opens it, but it lines up with the whole field and takes its width */
export default function PopoverAnchored() {
  const id = useId();

  return (
    <Popover>
      <PopoverAnchor asChild>
        <div className="flex w-full max-w-sm gap-2">
          <Input type="search" aria-label="Search files" placeholder="Search files…" />
          <PopoverTrigger asChild>
            <Button variant="outline" size="icon" aria-label="Filters">
              <SlidersHorizontalIcon aria-hidden />
            </Button>
          </PopoverTrigger>
        </div>
      </PopoverAnchor>
      <PopoverContent align="start" className="grid w-(--radix-popover-trigger-width) gap-4">
        <PopoverHeader>
          <PopoverTitle>Filters</PopoverTitle>
        </PopoverHeader>
        <div className="grid gap-3">
          {filters.map((filter) => (
            <div key={filter.key} className="flex items-center gap-2">
              <Checkbox id={`${id}-${filter.key}`} size="sm" defaultChecked={filter.on} />
              <label htmlFor={`${id}-${filter.key}`} className="text-sm">
                {filter.label}
              </label>
            </div>
          ))}
        </div>
        <div className="flex justify-end">
          <PopoverClose asChild>
            <Button size="sm">Done</Button>
          </PopoverClose>
        </div>
      </PopoverContent>
    </Popover>
  );
}
```

## Related

- [Tooltip](https://ui.fewtone.com/components/tooltip.md): An accessible React tooltip for Next.js and Tailwind CSS: a short hint on hover and keyboard focus, fast across a toolbar, built on Radix Tooltip.
- [Hover Card](https://ui.fewtone.com/components/hover-card.md): An accessible React hover card for Next.js and Tailwind CSS: a preview card for a link, like a profile on an @mention, built on Radix Hover Card.
- [Select](https://ui.fewtone.com/components/select.md): An accessible React select with a gliding highlight, groups, labels and a scrollable list, built on Radix Select.
