Skip to content
Dethink Components

ComponentsPopover

Popover

Show extra content next to a button or other trigger.

On this page

Use this component from the local workspace package. Follow the setup guide first. A public npm package and registry are not available yet.

View registry files

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 {
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverDescription,
  PopoverFooter,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@dethink/components";

export function PopoverBasic() {
  return (
    <div className="flex justify-center">
      <Popover>
        <PopoverTrigger variant="outline">Share dashboard</PopoverTrigger>
        <PopoverContent>
          <PopoverHeader>
            <PopoverTitle>Share dashboard</PopoverTitle>
            <PopoverDescription>
              Anyone in the workspace can view. Focus moves inside and returns
              to the trigger on close.
            </PopoverDescription>
          </PopoverHeader>
          <PopoverFooter>
            <PopoverClose variant="outline">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,
  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>
            <PopoverDescription className="px-[var(--dt-space-4)] py-[var(--dt-space-3)]">
              Anchored {placement} with an arrow.
            </PopoverDescription>
          </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 { useState } from "react";
import {
  Field,
  FieldControl,
  FieldLabel,
  Input,
  Popover,
  PopoverClose,
  PopoverContent,
  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);

  return (
    <div className="border-border mx-auto flex max-w-sm items-center justify-between rounded-lg border px-4 py-3">
      <div>
        <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));
          }
        }}
      >
        <PopoverTrigger variant="ghost" className="font-mono">
          ${limit}
        </PopoverTrigger>
        <PopoverContent placement="bottom end">
          <PopoverHeader>
            <PopoverTitle>Edit spend limit</PopoverTitle>
          </PopoverHeader>
          <div className="px-[var(--dt-space-4)] py-[var(--dt-space-2)]">
            <Field id="limit-draft">
              <FieldLabel>Limit (USD)</FieldLabel>
              <FieldControl asChild>
                <Input
                  type="number"
                  min={0}
                  value={draft}
                  onChange={(event) => setDraft(event.target.value)}
                />
              </FieldControl>
            </Field>
          </div>
          <PopoverFooter>
            <PopoverClose variant="ghost">Cancel</PopoverClose>
            <PopoverClose
              onPress={() => setLimit(Math.max(0, Number(draft) || 0))}
            >
              Save
            </PopoverClose>
          </PopoverFooter>
        </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