Skip to content
Dethink Components

ComponentsPopover

Popover

Show extra content next to a button or other trigger.

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files
  • React Aria (react-aria)

    Accessibility hooks and overlay utilities used by the implementation.

  • React Aria Components (react-aria-components)

    Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

Import the component into your page or component file.

Usage
import {
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@dethink/components";

Try the examples, then open the code to use them in your app.

Basic

Trigger, titled content, and a footer close button.

Show sourceexamples/popover/basic.tsx
examples/popover/basic.tsx
"use client";

import { Users, Share2 } from "lucide-react";
import {
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverDescription,
  PopoverFooter,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@dethink/components";

export function PopoverBasic() {
  return (
    <div className="flex justify-center">
      <Popover>
        <PopoverTrigger variant="outline">
          <Share2 aria-hidden="true" className="size-4" />
          Share dashboard
        </PopoverTrigger>
        <PopoverContent showArrow>
          <PopoverHeader>
            <PopoverTitle>Share dashboard</PopoverTitle>
            <PopoverDescription>
              Keep your team on the same page.
            </PopoverDescription>
          </PopoverHeader>
          <div className="border-border bg-muted/40 flex items-center gap-3 rounded-lg border p-3">
            <span className="border-border bg-background text-muted-foreground flex size-9 shrink-0 items-center justify-center rounded-md border">
              <Users aria-hidden="true" className="size-4" />
            </span>
            <div className="min-w-0 flex-1">
              <p className="text-sm font-medium">Workspace members</p>
              <p className="text-muted-foreground text-xs">
                Everyone in your workspace
              </p>
            </div>
            <span className="text-muted-foreground shrink-0 text-xs">
              Can view
            </span>
          </div>
          <PopoverFooter>
            <PopoverClose size="sm" variant="solid">
              Done
            </PopoverClose>
          </PopoverFooter>
        </PopoverContent>
      </Popover>
    </div>
  );
}

Placement and arrow

placement picks the side; showArrow points the panel at its trigger. Sides flip automatically when space runs out.

Show sourceexamples/popover/placement.tsx
examples/popover/placement.tsx
"use client";

import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@dethink/components";

const placements = ["top", "right", "bottom", "left"] as const;

export function PopoverPlacement() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      {placements.map((placement) => (
        <Popover key={placement}>
          <PopoverTrigger variant="soft" size="sm">
            {placement}
          </PopoverTrigger>
          <PopoverContent placement={placement} showArrow>
            <PopoverHeader>
              <PopoverTitle className="capitalize">
                {placement} placement
              </PopoverTitle>
              <PopoverDescription>
                The arrow follows the trigger. This panel flips sides when it
                needs more room.
              </PopoverDescription>
            </PopoverHeader>
          </PopoverContent>
        </Popover>
      ))}
    </div>
  );
}

Examples that combine components for common tasks.

Inline edit

The value on the page is the trigger. The controlled popover edits a draft, so Cancel discards and Save commits — the displayed value never flickers mid-edit.

Monthly spend limit

Alerts fire at 80% of the limit.

Show sourceexamples/popover/recipe-inline-edit.tsx
examples/popover/recipe-inline-edit.tsx
"use client";

import { useId, useState } from "react";
import { Pencil } from "lucide-react";
import {
  Button,
  Field,
  FieldControl,
  FieldLabel,
  FieldError,
  Input,
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverDescription,
  PopoverFooter,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@dethink/components";

/**
 * Inline edit: the value on the page is the trigger. A controlled popover
 * carries a draft, so Cancel discards and Save commits — the page value
 * never flickers mid-edit.
 */
export function PopoverRecipeInlineEdit() {
  const [limit, setLimit] = useState(500);
  const [draft, setDraft] = useState(String(limit));
  const [open, setOpen] = useState(false);
  const [submitted, setSubmitted] = useState(false);
  const fieldId = useId();
  const amount = Number(draft);
  const valid = draft.trim() !== "" && Number.isFinite(amount) && amount >= 0;

  return (
    <div className="border-border bg-background mx-auto flex w-full max-w-sm items-center justify-between gap-4 rounded-xl border p-4 shadow-sm">
      <div className="min-w-0">
        <p className="text-sm font-medium">Monthly spend limit</p>
        <p className="text-muted-foreground text-sm">
          Alerts fire at 80% of the limit.
        </p>
      </div>
      <Popover
        open={open}
        onOpenChange={(next) => {
          setOpen(next);
          if (next) {
            setDraft(String(limit));
            setSubmitted(false);
          }
        }}
      >
        <PopoverTrigger
          variant="outline"
          className="shrink-0 tabular-nums"
          aria-label={`Edit monthly spend limit, currently $${limit}`}
        >
          ${limit}
          <Pencil
            aria-hidden="true"
            className="text-muted-foreground size-3.5"
          />
        </PopoverTrigger>
        <PopoverContent placement="bottom end" showArrow>
          <form
            className="grid gap-4"
            noValidate
            onSubmit={(event) => {
              event.preventDefault();
              setSubmitted(true);
              if (!valid) return;
              setLimit(amount);
              setOpen(false);
            }}
          >
            <PopoverHeader>
              <PopoverTitle>Edit spend limit</PopoverTitle>
              <PopoverDescription>
                Set a monthly budget for your workspace.
              </PopoverDescription>
            </PopoverHeader>
            <Field id={fieldId} invalid={submitted && !valid}>
              <FieldLabel>Limit (USD)</FieldLabel>
              <FieldControl asChild>
                <Input
                  type="number"
                  min={0}
                  step="any"
                  required
                  value={draft}
                  onChange={(event) => setDraft(event.target.value)}
                />
              </FieldControl>
              {submitted && !valid ? (
                <FieldError>Enter an amount of 0 or more.</FieldError>
              ) : null}
            </Field>
            <PopoverFooter>
              <PopoverClose size="sm" variant="ghost">
                Cancel
              </PopoverClose>
              <Button type="submit" size="sm">
                Save changes
              </Button>
            </PopoverFooter>
          </form>
        </PopoverContent>
      </Popover>
    </div>
  );
}

Popover coordinates trigger and content; content carries placement and arrow options.

Popover anatomy
PropWhat it doesDefault
open / defaultOpen / onOpenChangeboolean / boolean / (open) => voidControlled or uncontrolled visibility.Not set
PopoverTrigger / PopoverClosevariant + size (Button API)Anchor button and close buttons; PopoverClose also accepts onPress for save-on-close patterns.Not set
PopoverContent — placement"top" | "bottom" | "left" | "right" (+ " start"/" end")Preferred side and alignment relative to the trigger."bottom"
PopoverContent — showArrowbooleanRenders a pointing arrow tied to the trigger.false
PopoverContent — offsetnumberDistance in pixels between trigger and panel.8
PopoverHeader / PopoverTitle / PopoverDescription / PopoverFootersection componentsAnatomy pieces; title and description label the popover dialog.Not set