Popover
Pływający panel przypięty do wyzwalacza, na niewielkie interaktywne treści — ustawienia, szybkie formularze, selektory — który nie blokuje strony.
import { SlidersHorizontalIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover"
const dimensions = [
{ id: "width", label: "Szerokość", value: "100%" },
{ id: "max-width", label: "Maks. szer.", value: "1280px" },
{ id: "height", label: "Wysokość", value: "auto" },
{ id: "max-height", label: "Maks. wys.", value: "none" },
]
export default function PopoverDemo() {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>
<SlidersHorizontalIcon data-icon="inline-start" />
Wymiary
</PopoverTrigger>
<PopoverContent className="w-80">
<PopoverHeader>
<PopoverTitle>Wymiary</PopoverTitle>
<PopoverDescription>
Ustaw rozmiar zaznaczonej ramki.
</PopoverDescription>
</PopoverHeader>
<FieldGroup className="gap-2.5">
{dimensions.map((dimension) => (
<Field
key={dimension.id}
orientation="horizontal"
className="grid grid-cols-[6rem_1fr]"
>
<FieldLabel
htmlFor={dimension.id}
className="font-normal text-muted-foreground"
>
{dimension.label}
</FieldLabel>
<Input
id={dimension.id}
size="sm"
defaultValue={dimension.value}
/>
</Field>
))}
</FieldGroup>
</PopoverContent>
</Popover>
)
}Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/popover.jsonUżycie
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover"<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Dimensions</PopoverTrigger>
<PopoverContent>
<PopoverHeader>
<PopoverTitle>Dimensions</PopoverTitle>
<PopoverDescription>Set the size of the selected frame.</PopoverDescription>
</PopoverHeader>
{/* … */}
</PopoverContent>
</Popover>Za pozycjonowanie popovera odpowiada Floating UI: popover otwiera się pod wyzwalaczem, przeskakuje na drugą stronę, gdy brakuje miejsca, i nie wychodzi poza obszar widoku. Pojawia się od strony wyzwalacza — to choreografia popup-motion, wspólna dla wszystkich pływających powierzchni prfct.
Przykłady
Szybkie wprowadzanie danych
Popover może zawierać mały formularz. Steruj nim przez open i onOpenChange, aby zamykał się po udanym wysłaniu, a potwierdzenie pokaż w powiadomieniu Toast.
"use client"
import * as React from "react"
import { MessageSquareIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Field, FieldLabel } from "@/components/ui/field"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/toast"
export default function PopoverFeedback() {
const [open, setOpen] = React.useState(false)
const [message, setMessage] = React.useState("")
function send(event: React.FormEvent) {
event.preventDefault()
setOpen(false)
setMessage("")
toast.add({ title: "Dziękujemy za opinię", type: "success" })
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger render={<Button variant="outline" />}>
<MessageSquareIcon data-icon="inline-start" />
Przekaż opinię
</PopoverTrigger>
<PopoverContent align="end" className="w-80">
<form onSubmit={send} className="flex flex-col gap-3">
<Field>
<FieldLabel htmlFor="feedback-message">
Co możemy poprawić na tej stronie?
</FieldLabel>
<Textarea
id="feedback-message"
placeholder="Pomysły, błędy albo cokolwiek, co cię spowolniło…"
value={message}
onChange={(event) => setMessage(event.target.value)}
className="min-h-24"
/>
</Field>
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground">
Odpowiemy na twój adres e-mail.
</span>
<Button type="submit" size="sm" disabled={!message.trim()}>
Wyślij
</Button>
</div>
</form>
</PopoverContent>
</Popover>
)
}Nagłówek
PopoverHeader, PopoverTitle i PopoverDescription nadają popoverowi tytuł, który nazywa go dla technologii wspomagających. Używaj ich zawsze, gdy treść zawiera więcej niż jedną kontrolkę.
import { SettingsIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover"
import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
const options = [
{
id: "wrap",
label: "Zawijaj długie wiersze",
description: "Miękkie zawijanie na szerokości edytora.",
checked: true,
},
{
id: "minimap",
label: "Pokaż minimapę",
description: "Podgląd całego pliku po prawej stronie.",
checked: false,
},
{
id: "format",
label: "Formatuj przy zapisie",
description: "Uruchamia Prettier przed każdym zapisem.",
checked: true,
},
]
export default function PopoverSettings() {
return (
<Popover>
<PopoverTrigger
render={
<Button variant="ghost" size="icon" aria-label="Ustawienia edytora" />
}
>
<SettingsIcon />
</PopoverTrigger>
<PopoverContent align="start" className="w-80 gap-4">
<PopoverHeader>
<PopoverTitle>Ustawienia edytora</PopoverTitle>
<PopoverDescription>
Dotyczą każdego otwieranego pliku.
</PopoverDescription>
</PopoverHeader>
<Separator />
<FieldGroup className="gap-4">
{options.map((option) => (
<Field key={option.id} orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor={option.id}>{option.label}</FieldLabel>
<FieldDescription>{option.description}</FieldDescription>
</FieldContent>
<Switch
id={option.id}
size="sm"
defaultChecked={option.checked}
/>
</Field>
))}
</FieldGroup>
</PopoverContent>
</Popover>
)
}Położenie
side ustawia popover po wybranej stronie wyzwalacza: top, right, bottom (domyślnie) lub left. Jeśli po preferowanej stronie brakuje miejsca, popover przeskakuje na przeciwną.
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverTrigger,
} from "@/components/ui/popover"
const sides = ["top", "right", "bottom", "left"] as const
export default function PopoverSides() {
return (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
{sides.map((side) => (
<Popover key={side}>
<PopoverTrigger
render={<Button variant="outline" className="capitalize" />}
>
{side}
</PopoverTrigger>
<PopoverContent side={side} className="w-56">
<PopoverDescription>
Otwiera się po stronie{" "}
<strong className="font-medium text-foreground">{side}</strong>, a gdy brakuje miejsca — po przeciwnej.
</PopoverDescription>
</PopoverContent>
</Popover>
))}
</div>
)
}Wyrównanie
align wyrównuje popover do początku (start), środka (center, domyślnie) lub końca (end) wyzwalacza. Wyrównanie do początku sprawdza się w menu czytanych od lewej do prawej, a do końca — przy wyzwalaczach blisko prawej krawędzi.
import { Button } from "@/components/ui/button"
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverTrigger,
} from "@/components/ui/popover"
const alignments = ["start", "center", "end"] as const
export default function PopoverAlign() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
{alignments.map((align) => (
<Popover key={align}>
<PopoverTrigger
render={<Button variant="outline" className="w-28 capitalize" />}
>
{align}
</PopoverTrigger>
<PopoverContent align={align} className="w-60">
<PopoverDescription>
Wyrównanie{" "}
<strong className="font-medium text-foreground">{align}</strong>{" "}
względem wyzwalacza.
</PopoverDescription>
</PopoverContent>
</Popover>
))}
</div>
)
}Wytyczne
Kiedy używać
- Do kontrolek działających na jednym elemencie: formatowania, wymiarów, filtrów pojedynczej kolumny.
- Do szybkiego wprowadzania danych, które nie wymaga pełnego okna dialogowego: notatki, zmiany nazwy, daty.
- Do wybierania: kolorów, emoji, dat.
Kiedy nie używać
- Do pokazania zwykłej etykiety tekstowej — użyj Tooltip.
- Do podglądu linku po najechaniu — użyj Hover Card.
- Do listy akcji — użyj Dropdown Menu, które zapewnia nawigację strzałkami i wybór przez wpisywanie.
- Do zadań, które wymagają skupienia i wyraźnego zakończenia — użyj Dialog.
Niech pozostanie mały
Popover to rzut oka, a nie miejsce docelowe. Jeśli jego treść wymaga przewijania, więcej niż kilku kontrolek albo przycisku Zapisz, wyrosła już z popovera. Trzymaj szerokość poniżej ~20 rem, a wysokość — w granicach kilku wierszy.
Dostępność
Wyzwalacz to przycisk z aria-expanded i aria-haspopup="dialog"; popup to niemodalne okno dialogowe, którego etykietą jest PopoverTitle, jeśli występuje.
| Klawisz | Działanie |
|---|---|
EnterSpacja | Na wyzwalaczu: otwiera popover i przenosi fokus na jego pierwszy fokusowalny element albo zamyka popover, gdy jest otwarty. |
Tab | Przechodzi przez kontrolki popovera i dalej, do reszty strony. |
Esc | Zamyka popover i przywraca fokus na wyzwalacz. |
- Domyślnie niemodalny. Strona pozostaje interaktywna; kliknięcie poza popoverem albo przeniesienie fokusu gdzie indziej go zamyka. Przekaż
modal, aby zablokować przewijanie strony i interakcje wskaźnikiem poza popoverem. Fokus jest zatrzymywany w środku, przymodali przymodal="trap-focus", tylko wtedy, gdy popup zawieraPopoverClose: bez przycisku, którym da się wyjść za pomocą klawiatury, Base UI pozwala klawiszowi Tab opuścić popover. - Nazwij go. Używaj
PopoverTitlew każdym popoverze z kontrolkami. Jeśli nie ma widocznego tytułu, dodajaria-labeldoPopoverContent. - Otwieranie po najechaniu trzeba włączyć.
openOnHoverna wyzwalaczu sprawia, że popover otwiera się także po najechaniu, ale zawsze otwiera się też po kliknięciu i z klawiatury — nigdy nie rób z najechania jedynej drogi dostępu.
Dokumentacja API
Popover
Komponent główny. Nie renderuje żadnego elementu. Przyjmuje wszystkie propsy Popover.Root z Base UI.
PopoverTrigger
Otwiera i zamyka popover. Renderuje <button>; użyj render, aby wyrenderować Button z prfct.
PopoverContent
Renderuje portal, element pozycjonujący i popup.
PopoverHeader, PopoverTitle, PopoverDescription
PopoverHeader układa tytuł i opis jeden pod drugim. PopoverTitle renderuje <h2>, który jest etykietą popovera; PopoverDescription renderuje <p>, który go opisuje.