Przejdź do treści

Alert Dialog

Modalne okno potwierdzenia, które zatrzymuje użytkownika przed akcją o poważnych lub nieodwracalnych skutkach. Zamknąć je można tylko, dokonując wyboru.

Kod źródłowyBase UIRejestr
"use client"

import * as React from "react"
import { Trash2Icon } from "lucide-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export default function AlertDialogDemo() {
  const [open, setOpen] = React.useState(false)

  return (
    <AlertDialog open={open} onOpenChange={setOpen}>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        <Trash2Icon data-icon="inline-start" />
        Usuń projekt
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogMedia variant="destructive">
            <Trash2Icon />
          </AlertDialogMedia>
          <AlertDialogTitle>Usunąć projekt „Atlas”?</AlertDialogTitle>
          <AlertDialogDescription>
            Projekt, jego 142 zgłoszenia i wszystkie wdrożenia zostaną trwale usunięte. Tej operacji nie można cofnąć.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Zachowaj projekt</AlertDialogCancel>
          <AlertDialogAction
            variant="destructive"
            onClick={() => setOpen(false)}
          >
            Usuń projekt
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Instalacja

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/alert-dialog.json

Użycie

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
const [open, setOpen] = React.useState(false)

<AlertDialog open={open} onOpenChange={setOpen}>
  <AlertDialogTrigger render={<Button variant="outline" />}>Delete project</AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete “Atlas”?</AlertDialogTitle>
      <AlertDialogDescription>This can’t be undone.</AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Keep project</AlertDialogCancel>
      <AlertDialogAction variant="destructive" onClick={deleteProject}>
        Delete project
      </AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
Przycisk akcji nie zamyka okna
AlertDialogCancel zamyka okno. AlertDialogAction to celowo zwykły Button: potwierdzona operacja często jest asynchroniczna, a okno powinno pozostać otwarte — z akcją w stanie loading — aż do jej powodzenia lub błędu. Kontroluj open i zamknij okno samodzielnie, gdy operacja się zakończy.

Przykłady

Podstawowy

Często wystarczą tytuł i opis. Opis mówi, co zostanie utracone, a przyciski — co robi każdy z wyborów.

"use client"

import * as React from "react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export default function AlertDialogBasic() {
  const [open, setOpen] = React.useState(false)

  return (
    <AlertDialog open={open} onOpenChange={setOpen}>
      <AlertDialogTrigger render={<Button variant="ghost" />}>
        Odrzuć szkic
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Odrzucić niezapisane zmiany?</AlertDialogTitle>
          <AlertDialogDescription>
            Ten wpis był edytowany 4 minuty temu. Jeśli teraz wyjdziesz, zmiany przepadną.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Kontynuuj edycję</AlertDialogCancel>
          <AlertDialogAction onClick={() => setOpen(false)}>
            Odrzuć zmiany
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Grafika

AlertDialogMedia dodaje ikonę, która nadaje ton, zanim ktokolwiek przeczyta tytuł. Dopasuj jej variant do skutków — destructive przy utracie danych, warning przy zakłóceniach, success przy potwierdzaniu czegoś dobrego, czego nie da się cofnąć, brand przy ważnych, ale bezpiecznych akcjach, default w pozostałych przypadkach — i nadaj przyciskowi akcji ten sam stopień wyróżnienia.

"use client"

import * as React from "react"
import { GitBranchIcon, LogOutIcon, RocketIcon } from "lucide-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

const confirmations = [
  {
    trigger: "Wyloguj wszędzie",
    media: "default",
    icon: LogOutIcon,
    title: "Wylogować się ze wszystkich urządzeń?",
    description:
      "Sesje na 4 urządzeniach, w tym na tym, zostaną zakończone. Aby zalogować się ponownie, potrzebne będzie hasło.",
    action: "Wyloguj wszędzie",
    actionVariant: "default",
  },
  {
    trigger: "Odłącz GitHuba",
    media: "warning",
    icon: GitBranchIcon,
    title: "Odłączyć GitHuba?",
    description:
      "Nowe commity przestaną uruchamiać wdrożenia. Istniejące wdrożenia pozostaną dostępne.",
    action: "Odłącz",
    actionVariant: "default",
  },
  {
    trigger: "Wdróż na produkcję",
    media: "brand",
    icon: RocketIcon,
    title: "Wdrożyć na produkcję?",
    description:
      "Commit 7f3a2c1 zastąpi wersję produkcyjną dla 12 408 aktywnych użytkowników. Poprzednią wersję możesz przywrócić w każdej chwili.",
    action: "Wdróż",
    actionVariant: "brand",
  },
] as const

function Confirmation({ item }: { item: (typeof confirmations)[number] }) {
  const [open, setOpen] = React.useState(false)
  const Icon = item.icon

  return (
    <AlertDialog open={open} onOpenChange={setOpen}>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        {item.trigger}
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogMedia variant={item.media}>
            <Icon />
          </AlertDialogMedia>
          <AlertDialogTitle>{item.title}</AlertDialogTitle>
          <AlertDialogDescription>{item.description}</AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Anuluj</AlertDialogCancel>
          <AlertDialogAction
            variant={item.actionVariant}
            onClick={() => setOpen(false)}
          >
            {item.action}
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

export default function AlertDialogMediaVariants() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      {confirmations.map((item) => (
        <Confirmation key={item.trigger} item={item} />
      ))}
    </div>
  )
}

Mały

size="sm" wyśrodkowuje treść i dzieli stopkę na dwa równe przyciski. Używaj go do krótkich pytań z dwiema możliwymi odpowiedziami — o uprawnienia czy szybkie zgody — zwłaszcza na urządzeniach mobilnych.

"use client"

import * as React from "react"
import { BellRingIcon } from "lucide-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"

export default function AlertDialogSmall() {
  const [open, setOpen] = React.useState(false)

  return (
    <AlertDialog open={open} onOpenChange={setOpen}>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Włącz alerty
      </AlertDialogTrigger>
      <AlertDialogContent size="sm">
        <AlertDialogHeader>
          <AlertDialogMedia variant="brand">
            <BellRingIcon />
          </AlertDialogMedia>
          <AlertDialogTitle>Włączyć alerty o kompilacjach?</AlertDialogTitle>
          <AlertDialogDescription>
            Powiadomimy cię, gdy wdrożenie się nie powiedzie.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Nie teraz</AlertDialogCancel>
          <AlertDialogAction onClick={() => setOpen(false)}>
            Włącz
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Potwierdzenie przez wpisanie

Przy akcjach, które za jednym razem niszczą dużo pracy, poproś o wpisanie nazwy tego, co ma zostać usunięte. Przycisk akcji pozostaje wyłączony, dopóki tekst się nie zgadza, co zapobiega potwierdzaniu z przyzwyczajenia.

"use client"

import * as React from "react"
import { TriangleAlertIcon } from "lucide-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Field, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

const workspace = "acme-production"

export default function AlertDialogTypedConfirmation() {
  const [open, setOpen] = React.useState(false)
  const [value, setValue] = React.useState("")
  const matches = value === workspace

  return (
    <AlertDialog
      open={open}
      onOpenChange={(next) => {
        setOpen(next)
        if (!next) setValue("")
      }}
    >
      <AlertDialogTrigger render={<Button variant="destructive" />}>
        Usuń przestrzeń roboczą
      </AlertDialogTrigger>
      <AlertDialogContent>
        <form
          className="flex flex-col gap-5"
          onSubmit={(event) => {
            event.preventDefault()
            if (matches) setOpen(false)
          }}
        >
          <AlertDialogHeader>
            <AlertDialogMedia variant="destructive">
              <TriangleAlertIcon />
            </AlertDialogMedia>
            <AlertDialogTitle>Usunąć tę przestrzeń roboczą?</AlertDialogTitle>
            <AlertDialogDescription>
              Trwale usuniemy 38 projektów, 214 członków i całą ich historię.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <Field>
            <FieldLabel
              htmlFor="confirm-workspace"
              className="font-normal text-muted-foreground"
            >
              <span>
                Aby potwierdzić, wpisz{" "}
                <strong className="font-mono font-medium text-foreground">
                  {workspace}
                </strong>{" "}
                w polu poniżej
              </span>
            </FieldLabel>
            <Input
              id="confirm-workspace"
              value={value}
              autoComplete="off"
              spellCheck={false}
              onChange={(event) => setValue(event.target.value)}
            />
          </Field>
          <AlertDialogFooter>
            <AlertDialogCancel>Anuluj</AlertDialogCancel>
            <AlertDialogAction
              type="submit"
              variant="destructive"
              disabled={!matches}
            >
              Usuń przestrzeń roboczą
            </AlertDialogAction>
          </AlertDialogFooter>
        </form>
      </AlertDialogContent>
    </AlertDialog>
  )
}

Akcja asynchroniczna

Nie zamykaj okna, gdy trwa żądanie: przełącz akcję w stan loading, wyłącz anulowanie i ignoruj próby zamknięcia, dopóki operacja się nie zakończy. Potem zamknij okno i potwierdź wynik powiadomieniem Toast.

"use client"

import * as React from "react"
import { KeyRoundIcon } from "lucide-react"

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { toast } from "@/components/ui/toast"

export default function AlertDialogAsync() {
  const [open, setOpen] = React.useState(false)
  const [pending, setPending] = React.useState(false)

  async function revoke() {
    setPending(true)
    // Keep the dialog open until the request settles.
    await new Promise((resolve) => setTimeout(resolve, 1500))
    setPending(false)
    setOpen(false)
    toast.add({ title: "Unieważniono klucz API", type: "success" })
  }

  return (
    <AlertDialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Unieważnij klucz
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogMedia variant="warning">
            <KeyRoundIcon />
          </AlertDialogMedia>
          <AlertDialogTitle>Unieważnić klucz „Wdrożenia CI”?</AlertDialogTitle>
          <AlertDialogDescription>
            Potoki korzystające z tego klucza zakończą się błędem przy następnym uruchomieniu. Nowy klucz możesz utworzyć później.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel disabled={pending}>Anuluj</AlertDialogCancel>
          <AlertDialogAction
            variant="destructive"
            loading={pending}
            onClick={revoke}
          >
            Unieważnij klucz
          </AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
<AlertDialog open={open} onOpenChange={(next) => !pending && setOpen(next)}>
  {/* … */}
  <AlertDialogCancel disabled={pending}>Cancel</AlertDialogCancel>
  <AlertDialogAction variant="destructive" loading={pending} onClick={revoke}>
    Revoke key
  </AlertDialogAction>
</AlertDialog>

Wytyczne

Kiedy używać

  • Przed akcjami nieodwracalnymi: usuwaniem, unieważnianiem, trwałym usuwaniem osób.
  • Przed akcjami o szerokim zasięgu skutków: wdrożeniem na produkcję, odłączeniem integracji, wylogowaniem ze wszystkich urządzeń.
  • Gdy wyjście oznaczałoby utratę pracy, której nie da się odzyskać.

Kiedy nie używać

  • Przy akcjach, które da się cofnąć — wykonaj je od razu i zaproponuj Cofnij w powiadomieniu Toast. Możliwość cofnięcia jest zawsze życzliwsza niż potwierdzenie.
  • Do zbierania danych lub wyświetlania informacji — użyj komponentu Dialog.
  • Do informowania, że coś poszło nie tak — użyj komponentu Alert w treści strony albo powiadomienia.

Potwierdzaj rzadko

Każde potwierdzenie uczy ludzi odruchowo przeklikiwać kolejne. Zachowaj okna potwierdzenia na nieliczne momenty, które na to zasługują, i mów w nich konkretnie: podaj nazwę, liczbę i skutek.

Nazwij wybory

Tytuł zadaje jedno pytanie. Przycisk akcji powtarza czasownik z tytułu, a przycisk anulowania mówi, co pozostanie bez zmian (Zachowaj projekt), albo jest zwykłym Anuluj, gdy nic bardziej konkretnego nie pasuje. Nigdy nie używaj Tak, Nie ani OK.

Usunąć 3 pliki?Znikną dla wszystkich.
DobrzePytanie i przyciski używają tego samego czasownika.
Czy na pewno?Tej akcji nie można cofnąć.
Źle„Tak” i „Nie” zmuszają do ponownego czytania pytania pod presją.

Kolejność i wyróżnienie

Umieść akcję na końcu — po prawej od sm wzwyż, na górze, gdy na urządzeniach mobilnych przyciski w stopce układają się jeden pod drugim — a anulowanie przed nią. Przycisku destructive używaj tylko do akcji nieodwracalnych: czerwień oznacza zaraz coś stracisz.

Dostępność

Alert Dialog realizuje wzorzec WAI-ARIA Alert Dialog. Base UI renderuje go z role="alertdialog"; tytuł staje się jego nazwą dostępną, a opis — opisem dostępnym. Dzięki temu czytnik ekranu ogłasza skutki od razu po otwarciu okna.

KlawiszDziałanie
EnterSpacja
Na wyzwalaczu: otwiera okno potwierdzenia.
Tab
Przechodzi między wyborami. Fokus nie może opuścić okna.
ShiftTab
Przenosi fokus wstecz; z pierwszego elementu przechodzi na ostatni.
Esc
Zamyka okno — tak samo jak wybór Anuluj — i przywraca fokus na wyzwalacz.
  • Bez zamykania kliknięciem obok. Kliknięcie przyciemnionego tła nie zamyka okna potwierdzenia: trzeba dokonać wyboru. Esc nadal działa jak Anuluj.
  • Tytuł i opis są wymagane. Tylko z nich użytkownik czytnika ekranu dowie się, co potwierdza.
  • Fokus. Fokus trafia na pierwszy element, który może go przyjąć — zwykle przycisk anulowania, czyli bezpieczny wybór — a po zamknięciu wraca na wyzwalacz.
  • Kolor nigdy nie jest jedynym sygnałem. Akcja nieodwracalna jest czerwona, a jej etykieta dodatkowo mówi Usuń.

Dokumentacja API

AlertDialog

Komponent główny. Nie renderuje żadnego elementu. Przyjmuje wszystkie propsy AlertDialog.Root z Base UI — te same co Dialog, bez modal i disablePointerDismissal, które są zawsze włączone.

PropTypDomyślnie
open

Czy okno jest otwarte. Steruj nim, gdy akcja jest asynchroniczna.

booleanBrak wartości domyślnej
defaultOpen

Czy okno jest początkowo otwarte (w trybie niekontrolowanym).

booleanfalse
onOpenChange

Wywoływana, gdy okno się otwiera lub zamyka.

(open: boolean, details) => voidBrak wartości domyślnej
onOpenChangeComplete

Wywoływana po zakończeniu animacji otwierania lub zamykania.

(open: boolean) => voidBrak wartości domyślnej
actionsRef

Imperatywny uchwyt do zamknięcia lub odmontowania okna.

RefObject<{ close, unmount }>Brak wartości domyślnej

AlertDialogContent

Renderuje portal, przyciemnione tło i samo okno. Dwa komponenty, z których korzysta — AlertDialogPortal i AlertDialogOverlay — też są eksportowane, na wypadek układów, w których rozmieszczasz je samodzielnie.

PropTypDomyślnie
size

sm wyśrodkowuje treść, zwęża okno i dzieli stopkę na dwa równe przyciski.

"default" | "sm""default"
initialFocus

Element, który otrzymuje fokus po otwarciu.

boolean | RefObject | (interaction) => HTMLElement | booleanBrak wartości domyślnej
finalFocus

Element, który otrzymuje fokus po zamknięciu.

boolean | RefObject | (interaction) => HTMLElement | booleanBrak wartości domyślnej

AlertDialogMedia

Ikona w kolorowym kółku, od breakpointu sm wzwyż wyświetlana obok tytułu, a w małych oknach i na węższych ekranach — nad nim.

PropTypDomyślnie
variant

Kolor kółka z ikoną. Dopasuj go do skutków akcji.

"default" | "destructive" | "warning" | "success" | "brand""default"

AlertDialogAction

Button z prfct. Przyjmuje wszystkie propsy Button — variant, loading, disabled — i nie zamyka okna.

AlertDialogCancel

Zamyka okno. Renderuje Button z prfct.

PropTypDomyślnie
variant

Stopień wyróżnienia przycisku.

Button variant"outline"
size

Rozmiar przycisku.

Button size"default"

AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter

Części układu. AlertDialogTitle renderuje <h2> i nadaje oknu nazwę; AlertDialogDescription renderuje <p> i je opisuje. AlertDialogFooter na małych ekranach układa wybory jeden pod drugim, a od sm wzwyż wyrównuje je do prawej.