HextaUI

Popover

Bir trigger'a sabitlenen, içeriğiyle birlikte düzgünce yeniden boyutlanan ve trigger'ın yönünü izleyen yüzen bir panel.

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

Bileşeni, HextaUI tema token'larını ve bileşenin bağımlı olduğu tüm HextaUI bileşenlerini ekler.

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

Boyutu değişen içerik

İçerik büyüdüğünde veya küçüldüğünde açılır pencere zıplamak yerine yüksekliğini animasyonla değiştirir. Yazma gibi sürekli değişimler içeriği doğrudan izler, böylece hiçbir şey geride kalmaz.

"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>
  )
}

Kontrollü

Kendi durumunuzdan yönetmek için open ve onOpenChange verin. İkinci argüman değişimin nedenini söyler: trigger-press, outside-press veya escape-key gibi.

"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>
  )
}

Yerleşim

side ve align tercih edilen konumu belirler. Yer olmadığında açılır pencere diğer tarafa çevrilir ve kenarlardan 8px uzakta kalarak ekranda kalmak için kayar.

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>
  )
}

Üzerine gelince aç

Önizleme kartları için tetikleyicide openOnHover ayarlayın. delay ve closeDelay, imleç üzerinden geçerken titremesini önler.

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>
  )
}

Ayrık tetikleyiciler

Ağacın herhangi bir yerindeki birden çok tetikleyici arasında tek bir popover paylaşmak için createPopoverHandle ile bir handle oluşturun. Her tetikleyici bir payload geçirir ve açılır pencere onu bir fonksiyon alt öğesiyle işler.

"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>
  )
}

Takvimle

Kendi dolgusunu getiren içeriğe uymak için className="w-auto p-0" kullanın. Açılır pencere, takvim ay değiştirdikçe onu izler.

"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>
  )
}

İç içe

Başka bir popover veya sheet içindeki popover, üst öğesinin üzerinde katmanlanır. Alt öğenin içindeki tıklamalar üst öğeyi açık tutar ve Escape yalnızca en üstteki katmanı kapatır.

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>
  )
}

Uzun içerik

Bölünmeyen metin açılır pencerenin içinde sarar. İçerik mevcut alandan uzunsa açılır pencere ekran dışına taşmak yerine kendi içinde kayar.

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>
  )
}

modal ile sayfa kaydırma kilitlenir ve dış tıklamalar yalnızca popover'ı kapatır. Odağın tuzağa alınabilmesi ve dokunmatik ekran okuyucuların bir çıkış yolu olması için içeride bir <PopoverClose /> işleyin.

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>
  )
}

Devre dışı

disabled bir tetikleyici popover'ını hiç açmaz.

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>
  )
}

Sağdan sola

Açılır pencere bir portal içinde işlense de onu açan tetikleyicinin yönünü alır. inline-end gibi mantıksal taraflar onunla birlikte döner.

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>
  )
}
TuşAction
EnterSpaceTetikleyicide popover'ı açar veya kapatır. Odak açılır pencereye geçer.
TabAçılır pencerenin içeriğinde gezinir. Modal olmayan bir popover'dan Tab ile çıkmak onu kapatır.
EscPopover'ı kapatır ve odağı tetikleyiciye döndürür.
  • <PopoverTitle /> ve <PopoverDescription /> açılır pencereyi ekran okuyucular için etiketler ve tanımlar. Açılır pencere bir cümleden fazlasını içerdiğinde mutlaka bir başlık ekleyin.
  • Odak açılırken ilk odaklanabilir öğeye, kapanırken tetikleyiciye geri döner. Bunu initialFocus ve finalFocus ile değiştirin.
  • Azaltılmış hareket açıkken açılır pencere ölçeklenmeden solar.

Base UI popover üzerine kuruludur. Her parça sardığı ilkelin prop'larını kabul eder.

PropTürVarsayılan
defaultOpen
booleanfalse
open
boolean–
onOpenChangedetails.reason değişikliğe neyin yol açtığını belirtir.
(open: boolean, details) => void–
onOpenChangeCompleteAçma ya da kapatma animasyonu bittikten sonra çağrılır.
(open: boolean) => void–
modaltrue sayfa kaydırmayı ve dış etkileşimi kilitler. trap-focus yalnızca odağı tuzağa alır.
boolean | "trap-focus"false
handleAyrık tetikleyicileri bağlar.
PopoverHandle<Payload>–
children
ReactNode | ({ payload }) => ReactNode–
PropTürVarsayılan
openOnHover
booleanfalse
delayHover'da açılmadan önceki milisaniye.
number300
closeDelayHover bittikten sonra kapanmadan önceki milisaniye.
number0
handle
PopoverHandle<Payload>–
payloadBu tetikleyici açtığında açılır pencereye iletilir.
Payload–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<button>
ÖznitelikAçıklama
data-slot="popover-trigger"CSS'te tetikleyiciyi hedefleyin.
data-popup-openPopover'ı açıkken bulunur.
data-pressedTetikleyiciye basılıyken bulunur.
data-disabledTrigger devre dışıyken bulunur.

Portal'ı, konumlandırıcıyı ve açılır pencereyi tek bir parçada işler.

PropTürVarsayılan
side
"top" | "right" | "bottom" | "left" | "inline-start" | "inline-end""bottom"
align
"start" | "center" | "end""center"
sideOffsetTetikleyici ile açılır pencere arasındaki boşluk.
number | (data) => number6
alignOffset
number | (data) => number0
collisionPaddingViewport kenarlarından bırakılan boşluk.
number | Rect8
collisionAvoidanceYer bittiğinde çevrilip çevrilmeyeceği, kaydırılıp kaydırılmayacağı veya hiçbirinin yapılmayacağı.
CollisionAvoidance–
collisionBoundary
Boundary–
anchorTrigger dışında bir şeye göre konumlandırın.
Element | RefObject | VirtualElement | () => Element–
sticky
booleanfalse
positionMethod
"absolute" | "fixed""absolute"
initialFocusPopover açıldığında odağın gittiği yer.
boolean | RefObject | (type) => HTMLElement | boolean–
finalFocusPopover kapandığında odağın gittiği yer.
boolean | RefObject | (type) => HTMLElement | boolean–
portalPropscontainer gibi portal prop'ları.
PortalProps–
classNameAçılır pencere varsayılan olarak w-72'dir.
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
ÖznitelikAçıklama
data-slot="popover-content"Açılır pencere.
data-slot="popover-positioner"Açılır pencereyi konumlandıran öğe.
data-openPopover açıkken bulunur.
data-starting-styleAçılır pencere animasyonla girerken bulunur.
data-ending-styleAçılır pencere animasyonla çıkarken bulunur.
data-sideAçılır pencerenin sonunda yerleştiği taraf.
data-alignAçılır pencerenin sonunda aldığı hizalama.
data-instantDeğişim animasyonlu olmamalıyken bulunur.
--transform-originAçılır pencerenin tetikleyicide ölçeklendiği nokta.
--available-widthTetikleyici ile viewport kenarı arasındaki boşluk.
--available-heightTetikleyici ile viewport kenarı arasındaki boşluk. Açılır pencerenin azami yüksekliği.
--anchor-widthTetikleyicinin genişliği.
--anchor-heightTetikleyicinin yüksekliği.

Başlığı ve açıklamayı üst üste dizen düz bir <div>.

ÖznitelikAçıklama
data-slot="popover-header"CSS'te başlığı hedefleyin.
PropTürVarsayılan
render
ReactElement | (props, state) => ReactElement<h2>
ÖznitelikAçıklama
data-slot="popover-title"Açılır pencereyi etiketler.
PropTürVarsayılan
render
ReactElement | (props, state) => ReactElement<p>
ÖznitelikAçıklama
data-slot="popover-description"Açılır pencereyi tanımlar.
PropTürVarsayılan
render
ReactElement | (props, state) => ReactElement<button>
ÖznitelikAçıklama
data-slot="popover-close"Basıldığında popover'ı kapatır.

createPopoverHandle<Payload>(), bir <Popover /> öğesini başka bir yerde render edilen tetikleyicilere bağlayan bir tanıtıcı döndürür. Bileşeninizin dışında, yalnızca bir kez oluşturun.

Kullanıldığı bloklar

Popover üzerine kurulan bloklar.