HextaUI

Popover

Un panneau flottant ancré à un déclencheur, qui se redimensionne en douceur avec son contenu et suit la direction du déclencheur.

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

Ajoute le composant, les tokens de thème HextaUI et les composants HextaUI dont il dépend.

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

Contenu qui change de taille

Quand le contenu grandit ou rétrécit, la popup anime sa hauteur au lieu de sauter. Les changements continus, comme la saisie, suivent directement le contenu pour que rien ne traîne.

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

Contrôlé

Passez open et onOpenChange pour le piloter depuis votre propre état. Le second argument indique pourquoi il a changé, par exemple trigger-press, outside-press ou 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>
  )
}

Placement

side et align définissent la position préférée. Faute de place, la popup passe de l'autre côté et se décale pour rester à l'écran, en gardant 8px des bords.

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

Ouverture au survol

Définissez openOnHover sur le déclencheur pour des cartes d'aperçu. delay et closeDelay évitent le scintillement quand le pointeur passe dessus.

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

Déclencheurs détachés

Créez un handle avec createPopoverHandle pour partager un même popover entre plusieurs déclencheurs situés n'importe où dans l'arbre. Chaque déclencheur passe un payload, et la popup l'affiche via une fonction enfant.

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

Avec un calendrier

Utilisez className="w-auto p-0" pour ajuster un contenu qui apporte son propre padding. La popup suit le calendrier quand il change de mois.

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

Imbriqué

Un popover dans un autre popover ou dans une sheet se superpose à son parent. Les clics dans l'enfant gardent le parent ouvert, et Escape ne ferme que la couche la plus haute.

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

Contenu long

Le texte sans espace passe à la ligne dans la popup. Quand le contenu dépasse l'espace disponible, la popup défile en interne au lieu de sortir de l'écran.

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

Avec modal, le défilement de la page est verrouillé et les clics extérieurs ne font que fermer le popover. Rendez un <PopoverClose /> à l'intérieur pour que le focus puisse être piégé et que les lecteurs d'écran tactiles aient une issue.

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

Désactivé

Un déclencheur disabled n'ouvre jamais son popover.

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

De droite à gauche

La popup reprend la direction du déclencheur qui l'a ouverte, même si elle est rendue dans un portail. Les côtés logiques comme inline-end s'inversent avec elle.

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>
  )
}
ToucheAction
EnterSpaceSur le déclencheur, ouvre ou ferme le popover. Le focus passe dans la popup.
TabParcourt le contenu de la popup. Sortir par Tab d'un popover non modal le ferme.
EscFerme le popover et rend le focus au déclencheur.
  • <PopoverTitle /> et <PopoverDescription /> nomment et décrivent la popup pour les lecteurs d'écran. Incluez un titre dès que la popup contient plus d'une phrase.
  • Le focus passe au premier élément focalisable à l'ouverture et revient au déclencheur à la fermeture. Modifiez cela avec initialFocus et finalFocus.
  • Avec la réduction des animations, la popup apparaît en fondu sans changement d'échelle.

Construit sur le popover de Base UI. Chaque partie accepte les props de la primitive qu'elle enveloppe.

PropTypePar défaut
defaultOpen
booleanfalse
open
boolean–
onOpenChangedetails.reason indique la cause du changement.
(open: boolean, details) => void–
onOpenChangeCompleteAppelé à la fin de l’animation d’ouverture ou de fermeture.
(open: boolean) => void–
modaltrue verrouille le défilement de la page et l'interaction extérieure. trap-focus ne piège que le focus.
boolean | "trap-focus"false
handleRelie des déclencheurs détachés.
PopoverHandle<Payload>–
children
ReactNode | ({ payload }) => ReactNode–
PropTypePar défaut
openOnHover
booleanfalse
delayMillisecondes avant l'ouverture au survol.
number300
closeDelayMillisecondes avant la fermeture à la fin du survol.
number0
handle
PopoverHandle<Payload>–
payloadTransmis à la popup quand ce déclencheur l'ouvre.
Payload–
disabled
booleanfalse
render
ReactElement | (props, state) => ReactElement<button>
AttributDescription
data-slot="popover-trigger"Ciblez le déclencheur en CSS.
data-popup-openPrésent tant que son popover est ouvert.
data-pressedPrésent tant que le déclencheur est enfoncé.
data-disabledPrésent lorsque le déclencheur est désactivé.

Rend le portail, le positionneur et la popup en une seule partie.

PropTypePar défaut
side
"top" | "right" | "bottom" | "left" | "inline-start" | "inline-end""bottom"
align
"start" | "center" | "end""center"
sideOffsetEspace entre le déclencheur et la popup.
number | (data) => number6
alignOffset
number | (data) => number0
collisionPaddingEspace conservé par rapport aux bords du viewport.
number | Rect8
collisionAvoidanceFaut-il inverser, décaler ou ne rien faire quand la place manque.
CollisionAvoidance–
collisionBoundary
Boundary–
anchorSe positionne par rapport à autre chose que le déclencheur.
Element | RefObject | VirtualElement | () => Element–
sticky
booleanfalse
positionMethod
"absolute" | "fixed""absolute"
initialFocusOù va le focus quand le popover s'ouvre.
boolean | RefObject | (type) => HTMLElement | boolean–
finalFocusOù va le focus quand le popover se ferme.
boolean | RefObject | (type) => HTMLElement | boolean–
portalPropsProps du portail, comme container.
PortalProps–
classNameLa popup fait w-72 par défaut.
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AttributDescription
data-slot="popover-content"Le popup.
data-slot="popover-positioner"L'élément qui positionne la popup.
data-openPrésent tant que le popover est ouvert.
data-starting-stylePrésent pendant l'animation d'entrée de la popup.
data-ending-stylePrésent pendant l'animation de sortie de la popup.
data-sideLe côté sur lequel la popup s'est placée.
data-alignL'alignement finalement retenu par la popup.
data-instantPrésent quand le changement ne doit pas être animé.
--transform-originLe point à partir duquel la popup change d'échelle, au niveau du déclencheur.
--available-widthEspace entre le déclencheur et le bord du viewport.
--available-heightEspace entre le déclencheur et le bord du viewport. La hauteur maximale de la popup.
--anchor-widthLa largeur du déclencheur.
--anchor-heightLa hauteur du déclencheur.

Un simple <div> qui empile le titre et la description.

AttributDescription
data-slot="popover-header"Ciblez l’en-tête en CSS.
PropTypePar défaut
render
ReactElement | (props, state) => ReactElement<h2>
AttributDescription
data-slot="popover-title"Nomme la popup.
PropTypePar défaut
render
ReactElement | (props, state) => ReactElement<p>
AttributDescription
data-slot="popover-description"Décrit la popup.
PropTypePar défaut
render
ReactElement | (props, state) => ReactElement<button>
AttributDescription
data-slot="popover-close"Ferme le popover quand on appuie dessus.

createPopoverHandle<Payload>() renvoie un handle qui relie un <Popover /> à des déclencheurs rendus ailleurs. Créez-le une seule fois, en dehors de votre composant.

Utilisé dans les blocks

Des blocks qui s’appuient sur Popover.