HextaUI

Popover

Ein schwebendes Panel, das an einem Trigger verankert ist, sich sanft mit seinem Inhalt in der Größe ändert und der Richtung des Triggers folgt.

import { IconAdjustmentsHorizontal } from "@tabler/icons-react"

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

const field =
  "h-8 w-full min-w-0 rounded-md border bg-transparent px-2 text-sm outline-none focus-visible:outline-hidden focus-visible:ring-3 focus-visible:ring-focus-ring pointer-coarse:h-11 pointer-coarse:text-lg"

export function PopoverDemo() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="outline" />}>
        <IconAdjustmentsHorizontal />
        Dimensions
      </PopoverTrigger>
      <PopoverContent>
        <PopoverHeader>
          <PopoverTitle>Dimensions</PopoverTitle>
          <PopoverDescription>
            Set the dimensions for the layer.
          </PopoverDescription>
        </PopoverHeader>
        <div className="grid grid-cols-[5rem_minmax(0,1fr)] items-center gap-2">
          <label htmlFor="popover-width" className="text-sm">
            Width
          </label>
          <input id="popover-width" className={field} defaultValue="100%" />
          <label htmlFor="popover-height" className="text-sm">
            Height
          </label>
          <input id="popover-height" className={field} defaultValue="25px" />
        </div>
        <div className="flex justify-end gap-2">
          <PopoverClose render={<Button variant="ghost" size="sm" />}>
            Cancel
          </PopoverClose>
          <PopoverClose render={<Button size="sm" />}>Apply</PopoverClose>
        </div>
      </PopoverContent>
    </Popover>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/popover.json

Fügt die Komponente, die HextaUI-Theme-Tokens und alle HextaUI-Komponenten hinzu, von denen sie abhängt.

import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"
<Popover>
  <PopoverTrigger render={<Button variant="outline" />}>
    Open
  </PopoverTrigger>
  <PopoverContent>
    <PopoverHeader>
      <PopoverTitle>Dimensions</PopoverTitle>
      <PopoverDescription>Set the dimensions for the layer.</PopoverDescription>
    </PopoverHeader>
  </PopoverContent>
</Popover>
Popover
├── PopoverTrigger
└── PopoverContent
    ├── PopoverHeader
    │   ├── PopoverTitle
    │   └── PopoverDescription
    └── PopoverClose

Inhalt, der die Größe ändert

Wächst oder schrumpft der Inhalt, animiert das Popup seine Höhe, statt zu springen. Kontinuierliche Änderungen wie Tippen folgen dem Inhalt direkt, sodass nichts hinterherhinkt.

"use client"

import * as React from "react"
import { IconBell } from "@tabler/icons-react"

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

export function PopoverResizing() {
  const [rows, setRows] = React.useState(1)
  const [loading, setLoading] = React.useState(false)
  const timer = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  React.useEffect(() => () => clearTimeout(timer.current), [])

  return (
    <Popover
      onOpenChange={(open) => {
        if (open) {
          setRows(1)
          setLoading(true)
          clearTimeout(timer.current)
          timer.current = setTimeout(() => setLoading(false), 700)
        }
      }}
    >
      <PopoverTrigger render={<Button variant="outline" />}>
        <IconBell />
        Notifications
      </PopoverTrigger>
      <PopoverContent>
        <PopoverHeader>
          <PopoverTitle>Notifications</PopoverTitle>
          <PopoverDescription>
            The height animates as content loads and grows.
          </PopoverDescription>
        </PopoverHeader>
        {loading ? (
          <Skeleton className="h-10 w-full" />
        ) : (
          <ul className="flex flex-col gap-2">
            {Array.from({ length: rows }, (_, index) => (
              <li
                key={index}
                className="rounded-md bg-muted px-2.5 py-2 text-sm"
              >
                Deploy #{1200 + index} finished in {12 + index}s
              </li>
            ))}
          </ul>
        )}
        <div className="flex gap-2">
          <Button
            variant="outline"
            size="sm"
            disabled={loading}
            onClick={() => setRows(Math.min(rows + 2, 12))}
          >
            Load more
          </Button>
          <Button
            variant="ghost"
            size="sm"
            disabled={loading || rows === 1}
            onClick={() => setRows(1)}
          >
            Collapse
          </Button>
        </div>
      </PopoverContent>
    </Popover>
  )
}

Kontrolliert

Übergib open und onOpenChange, um es aus deinem eigenen State zu steuern. Das zweite Argument nennt den Grund der Änderung, etwa trigger-press, outside-press oder escape-key.

"use client"

import * as React from "react"

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

export function PopoverControlled() {
  const [open, setOpen] = React.useState(false)
  const [reason, setReason] = React.useState("none")

  return (
    <div className="flex flex-col items-center gap-3">
      <div className="flex flex-wrap justify-center gap-2">
        <Popover
          open={open}
          onOpenChange={(next, details) => {
            setOpen(next)
            setReason(details.reason)
          }}
        >
          <PopoverTrigger render={<Button variant="outline" />}>
            Controlled
          </PopoverTrigger>
          <PopoverContent>
            <PopoverHeader>
              <PopoverTitle>Controlled</PopoverTitle>
              <PopoverDescription>
                The open state lives in the parent.
              </PopoverDescription>
            </PopoverHeader>
          </PopoverContent>
        </Popover>
        <Button variant="ghost" onClick={() => setOpen(!open)}>
          Toggle from outside
        </Button>
      </div>
      <p className="text-sm text-muted-foreground">
        Open: {String(open)} · Last reason: {reason}
      </p>
    </div>
  )
}

Platzierung

side und align legen die bevorzugte Position fest. Ist kein Platz, klappt das Popup auf die andere Seite und verschiebt sich, um auf dem Bildschirm zu bleiben, mit 8px Abstand zu den Rändern.

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

const sides = ["top", "right", "bottom", "left"] as const
const aligns = ["start", "center", "end"] as const

export function PopoverPlacement() {
  return (
    <div className="flex flex-col items-center gap-3">
      <div className="flex flex-wrap justify-center gap-2">
        {sides.map((side) => (
          <Popover key={side}>
            <PopoverTrigger render={<Button variant="outline" size="sm" />}>
              {side}
            </PopoverTrigger>
            <PopoverContent side={side} className="w-48">
              <PopoverTitle>Side: {side}</PopoverTitle>
            </PopoverContent>
          </Popover>
        ))}
      </div>
      <div className="flex flex-wrap justify-center gap-2">
        {aligns.map((align) => (
          <Popover key={align}>
            <PopoverTrigger render={<Button variant="outline" size="sm" />}>
              Align {align}
            </PopoverTrigger>
            <PopoverContent align={align} className="w-64">
              <PopoverTitle>Align: {align}</PopoverTitle>
            </PopoverContent>
          </Popover>
        ))}
      </div>
    </div>
  )
}

Per Hover öffnen

Setze openOnHover am Trigger für Vorschaukarten. delay und closeDelay verhindern Flackern, wenn der Zeiger darüberfährt.

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"

export function PopoverHover() {
  return (
    <Popover>
      <PopoverTrigger
        openOnHover
        delay={200}
        closeDelay={150}
        render={<Button variant="link" />}
      >
        @preetsuthar
      </PopoverTrigger>
      <PopoverContent align="start">
        <div className="flex items-start gap-3">
          <Avatar>
            <AvatarFallback>PS</AvatarFallback>
          </Avatar>
          <PopoverHeader>
            <PopoverTitle>Preet Suthar</PopoverTitle>
            <PopoverDescription>
              Building HextaUI. Opens on hover after 200ms and stays open while
              the pointer is inside.
            </PopoverDescription>
          </PopoverHeader>
        </div>
      </PopoverContent>
    </Popover>
  )
}

Losgelöste Trigger

Erstelle mit createPopoverHandle ein Handle, um ein Popover zwischen mehreren Triggern an beliebiger Stelle im Baum zu teilen. Jeder Trigger übergibt einen payload, und das Popup rendert ihn über ein Funktions-Child.

"use client"

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

const people = createPopoverHandle<{ name: string; role: string }>()

export function PopoverDetached() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <PopoverTrigger
        handle={people}
        payload={{ name: "Ada Lovelace", role: "Analyst" }}
        render={<Button variant="outline" size="sm" />}
      >
        Ada
      </PopoverTrigger>
      <PopoverTrigger
        handle={people}
        payload={{
          name: "Grace Hopper",
          role: "Rear admiral and the person who popularised the term debugging",
        }}
        render={<Button variant="outline" size="sm" />}
      >
        Grace
      </PopoverTrigger>
      <PopoverTrigger
        handle={people}
        payload={{ name: "Alan Turing", role: "Mathematician" }}
        render={<Button variant="outline" size="sm" />}
      >
        Alan
      </PopoverTrigger>
      <Popover handle={people}>
        {({ payload }) => (
          <PopoverContent>
            <PopoverHeader>
              <PopoverTitle>{payload?.name}</PopoverTitle>
              <PopoverDescription>{payload?.role}</PopoverDescription>
            </PopoverHeader>
          </PopoverContent>
        )}
      </Popover>
    </div>
  )
}

Mit einem Kalender

Verwende className="w-auto p-0" für Inhalt, der sein eigenes Padding mitbringt. Das Popup folgt dem Kalender, wenn er den Monat wechselt.

"use client"

import * as React from "react"
import { IconCalendar } from "@tabler/icons-react"
import { format } from "date-fns"

import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export function PopoverCalendar() {
  const [date, setDate] = React.useState<Date>()
  const [open, setOpen] = React.useState(false)

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger render={<Button variant="outline" />}>
        <IconCalendar />
        {date ? format(date, "PPP") : "Pick a date"}
      </PopoverTrigger>
      <PopoverContent className="w-auto p-0" align="start">
        <Calendar
          mode="single"
          selected={date}
          onSelect={(next) => {
            setDate(next)
            setOpen(false)
          }}
          defaultMonth={date}
        />
      </PopoverContent>
    </Popover>
  )
}

Verschachtelt

Ein Popover in einem anderen Popover oder einem Sheet liegt über seinem Elternteil. Klicks im Kind lassen das Elternteil offen, und Escape schließt nur die oberste Ebene.

import { IconInfoCircle } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

export function PopoverNested() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <Popover>
        <PopoverTrigger render={<Button variant="outline" />}>
          Popover in popover
        </PopoverTrigger>
        <PopoverContent>
          <PopoverHeader>
            <PopoverTitle>Parent</PopoverTitle>
            <PopoverDescription>
              Clicking inside the child keeps this one open.
            </PopoverDescription>
          </PopoverHeader>
          <Popover>
            <PopoverTrigger render={<Button variant="outline" size="sm" />}>
              <IconInfoCircle />
              More info
            </PopoverTrigger>
            <PopoverContent side="right" className="w-56">
              <PopoverTitle>Child</PopoverTitle>
              <PopoverClose render={<Button size="sm" variant="ghost" />}>
                Close child
              </PopoverClose>
            </PopoverContent>
          </Popover>
        </PopoverContent>
      </Popover>
      <Sheet>
        <SheetTrigger render={<Button variant="outline" />}>
          Popover in a sheet
        </SheetTrigger>
        <SheetContent>
          <SheetHeader>
            <SheetTitle>Sheet</SheetTitle>
            <SheetDescription>
              The popover layers above the sheet, and Escape closes only the
              popover.
            </SheetDescription>
          </SheetHeader>
          <SheetBody>
            <Popover>
              <PopoverTrigger render={<Button variant="outline" />}>
                Open popover
              </PopoverTrigger>
              <PopoverContent>
                <PopoverTitle>Inside a sheet</PopoverTitle>
              </PopoverContent>
            </Popover>
          </SheetBody>
        </SheetContent>
      </Sheet>
    </div>
  )
}

Langer Inhalt

Ununterbrochener Text bricht im Popup um. Ist der Inhalt höher als der verfügbare Platz, scrollt das Popup in sich, statt aus dem Bildschirm zu laufen.

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

export function PopoverLongContent() {
  return (
    <div className="flex flex-wrap justify-center gap-2">
      <Popover>
        <PopoverTrigger render={<Button variant="outline" />}>
          Unbroken text
        </PopoverTrigger>
        <PopoverContent>
          <PopoverHeader>
            <PopoverTitle>
              Supercalifragilisticexpialidocious-project-archive-2026-final-v3
            </PopoverTitle>
            <PopoverDescription>
              https://example.com/a/really/long/url/without/any/spaces/at/all/in/it
              — مرحبا بالعالم — 日本語のテキスト 👩‍👩‍👧‍👦
            </PopoverDescription>
          </PopoverHeader>
        </PopoverContent>
      </Popover>
      <Popover>
        <PopoverTrigger render={<Button variant="outline" />}>
          Taller than the screen
        </PopoverTrigger>
        <PopoverContent>
          <PopoverTitle>Changelog</PopoverTitle>
          {Array.from({ length: 40 }, (_, index) => (
            <p key={index} className="text-sm text-muted-foreground">
              v1.{40 - index}.0 — fixes and improvements
            </p>
          ))}
        </PopoverContent>
      </Popover>
    </div>
  )
}

Mit modal ist das Scrollen der Seite gesperrt, und Klicks außerhalb schließen nur das Popover. Rendere darin ein <PopoverClose />, damit der Fokus gefangen werden kann und Touch-Screenreader einen Ausweg haben.

import { IconX } from "@tabler/icons-react"

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

export function PopoverModal() {
  return (
    <Popover modal>
      <PopoverTrigger render={<Button variant="outline" />}>
        Modal
      </PopoverTrigger>
      <PopoverContent>
        <div className="flex items-start justify-between gap-2">
          <PopoverHeader>
            <PopoverTitle>Modal popover</PopoverTitle>
            <PopoverDescription>
              Page scroll is locked and outside clicks only dismiss.
            </PopoverDescription>
          </PopoverHeader>
          <PopoverClose
            aria-label="Close"
            render={<Button variant="ghost" size="icon-sm" />}
          >
            <IconX />
          </PopoverClose>
        </div>
      </PopoverContent>
    </Popover>
  )
}

Deaktiviert

Ein disabled Trigger öffnet sein Popover nie.

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

export function PopoverDisabled() {
  return (
    <Popover>
      <PopoverTrigger disabled render={<Button variant="outline" />}>
        Disabled
      </PopoverTrigger>
      <PopoverContent>
        <PopoverTitle>Never shown</PopoverTitle>
      </PopoverContent>
    </Popover>
  )
}

Rechts nach links

Das Popup übernimmt die Richtung des Triggers, der es geöffnet hat, obwohl es in einem Portal rendert. Logische Seiten wie inline-end drehen sich mit.

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

export function PopoverRtl() {
  return (
    <div dir="rtl" className="flex flex-wrap justify-center gap-2">
      <Popover>
        <PopoverTrigger render={<Button variant="outline" />}>
          الأبعاد
        </PopoverTrigger>
        <PopoverContent align="start">
          <PopoverHeader>
            <PopoverTitle>الأبعاد</PopoverTitle>
            <PopoverDescription>اضبط أبعاد الطبقة.</PopoverDescription>
          </PopoverHeader>
          <div className="flex justify-end">
            <PopoverClose render={<Button size="sm" />}>تطبيق</PopoverClose>
          </div>
        </PopoverContent>
      </Popover>
      <Popover>
        <PopoverTrigger render={<Button variant="outline" />}>
          inline-end
        </PopoverTrigger>
        <PopoverContent side="inline-end" className="w-48">
          <PopoverTitle>يفتح نحو النهاية</PopoverTitle>
        </PopoverContent>
      </Popover>
    </div>
  )
}
TasteAktion
EnterSpaceAm Trigger öffnet oder schließt es das Popover. Der Fokus wandert ins Popup.
TabBewegt sich durch den Inhalt des Popups. Wer ein nicht-modales Popover per Tab verlässt, schließt es.
EscSchließt das Popover und gibt den Fokus an den Trigger zurück.
  • <PopoverTitle /> und <PopoverDescription /> beschriften und beschreiben das Popup für Screenreader. Füge einen Titel hinzu, sobald das Popup mehr als einen Satz enthält.
  • Der Fokus wandert beim Öffnen auf das erste fokussierbare Element und beim Schließen zurück zum Trigger. Ändere das mit initialFocus und finalFocus.
  • Bei aktivierter reduzierter Bewegung blendet das Popup ohne Skalierung ein.

Gebaut auf dem Base UI Popover. Jeder Teil akzeptiert die Props des Primitivs, das er umschließt.

PropTypStandard
defaultOpen
booleanfalse
open
boolean–
onOpenChangedetails.reason gibt an, was die Änderung ausgelöst hat.
(open: boolean, details) => void–
onOpenChangeCompleteWird aufgerufen, nachdem die Öffnen- oder Schließen-Animation endet.
(open: boolean) => void–
modaltrue sperrt Seitenscroll und Interaktion außerhalb. trap-focus fängt nur den Fokus.
boolean | "trap-focus"false
handleVerbindet losgelöste Trigger.
PopoverHandle<Payload>–
children
ReactNode | ({ payload }) => ReactNode–
PropTypStandard
openOnHover
booleanfalse
delayMillisekunden bis zum Öffnen bei Hover.
number300
closeDelayMillisekunden bis zum Schließen, nachdem der Hover endet.
number0
handle
PopoverHandle<Payload>–
payloadWird an das Popup übergeben, wenn dieser Trigger es öffnet.
Payload–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-slot="popover-trigger"Den Trigger in CSS ansprechen.
data-popup-openVorhanden, solange sein Popover offen ist.
data-pressedVorhanden, solange der Trigger gedrückt ist.
data-disabledVorhanden, wenn der Trigger deaktiviert ist.

Rendert Portal, Positioner und Popup in einem Teil.

PropTypStandard
side
"top" | "right" | "bottom" | "left" | "inline-start" | "inline-end""bottom"
align
"start" | "center" | "end""center"
sideOffsetAbstand zwischen Trigger und Popup.
number | (data) => number6
alignOffset
number | (data) => number0
collisionPaddingAbstand, der zu den Rändern des Viewports eingehalten wird.
number | Rect8
collisionAvoidanceOb umgeklappt, verschoben oder keins von beidem, wenn der Platz ausgeht.
CollisionAvoidance–
collisionBoundary
Boundary–
anchorAn etwas anderem als dem Trigger positionieren.
Element | RefObject | VirtualElement | () => Element–
sticky
booleanfalse
positionMethod
"absolute" | "fixed""absolute"
initialFocusWohin der Fokus geht, wenn das Popover öffnet.
boolean | RefObject | (type) => HTMLElement | boolean–
finalFocusWohin der Fokus geht, wenn das Popover schließt.
boolean | RefObject | (type) => HTMLElement | boolean–
portalPropsProps für das Portal, etwa container.
PortalProps–
classNameDas Popup ist standardmäßig w-72.
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AttributBeschreibung
data-slot="popover-content"Das Popup.
data-slot="popover-positioner"Das Element, das das Popup positioniert.
data-openVorhanden, solange das Popover offen ist.
data-starting-styleVorhanden, solange das Popup einblendet.
data-ending-styleVorhanden, solange das Popup ausblendet.
data-sideDie Seite, auf der das Popup am Ende gelandet ist.
data-alignDie Ausrichtung, die das Popup am Ende erhalten hat.
data-instantVorhanden, wenn die Änderung nicht animiert werden soll.
--transform-originDer Punkt am Trigger, von dem aus das Popup skaliert.
--available-widthAbstand zwischen Trigger und Viewport-Rand.
--available-heightAbstand zwischen Trigger und Viewport-Rand. Die maximale Höhe des Popups.
--anchor-widthDie Breite des Triggers.
--anchor-heightDie Höhe des Triggers.

Ein einfaches <div>, das Titel und Beschreibung stapelt.

AttributBeschreibung
data-slot="popover-header"Den Header in CSS ansprechen.
PropTypStandard
render
ReactElement | (props, state) => ReactElement<h2>
AttributBeschreibung
data-slot="popover-title"Beschriftet das Popup.
PropTypStandard
render
ReactElement | (props, state) => ReactElement<p>
AttributBeschreibung
data-slot="popover-description"Beschreibt das Popup.
PropTypStandard
render
ReactElement | (props, state) => ReactElement<button>
AttributBeschreibung
data-slot="popover-close"Schließt das Popover beim Drücken.

createPopoverHandle<Payload>() gibt ein Handle zurück, das einen <Popover /> mit an anderer Stelle gerenderten Triggern verbindet. Erstelle es einmal, außerhalb deiner Komponente.

In Blocks verwendet

Blocks, die auf Popover aufbauen.