HextaUI

Alert dialog

破壊的または重要な操作のための確認ダイアログです。非同期処理を待ち、スマートフォンではボトムシートになります。

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

export function AlertDialogDemo() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Show dialog
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Publish changes?</AlertDialogTitle>
          <AlertDialogDescription>
            Your changes will be visible to everyone with the link.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction>Publish</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/alert-dialog.json

コンポーネント、HextaUIのテーマトークン、依存するHextaUIコンポーネントを追加します。

import {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogTitle,
  AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
<AlertDialog>
  <AlertDialogTrigger render={<Button variant="outline" />}>
    Delete
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete this project?</AlertDialogTitle>
      <AlertDialogDescription>This can’t be undone.</AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction>Delete</AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>
AlertDialog
├── AlertDialogTrigger
└── AlertDialogContent
    ├── AlertDialogHeader
    │   ├── AlertDialogMedia
    │   ├── AlertDialogTitle
    │   └── AlertDialogDescription
    └── AlertDialogFooter
        ├── AlertDialogCancel
        └── AlertDialogAction

非同期アクション

<AlertDialogAction /> の onClick から Promise を返します。ボタンにスピナーが表示され、Promise が完了するまでダイアログは閉じられません。resolve されると閉じ、reject されると開いたままになるため、やり直すことができます。

"use client"

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

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

function wait(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms))
}

export function AlertDialogAsyncAction() {
  const [shouldFail, setShouldFail] = React.useState(false)
  const [attempts, setAttempts] = React.useState(0)

  return (
    <div className="flex flex-wrap items-center gap-2">
      <AlertDialog onOpenChange={(open) => open && setAttempts(0)}>
        <AlertDialogTrigger render={<Button variant="destructive" />}>
          Delete project
        </AlertDialogTrigger>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogMedia variant="destructive">
              <IconTrash />
            </AlertDialogMedia>
            <AlertDialogTitle>Delete “Acme website”?</AlertDialogTitle>
            <AlertDialogDescription>
              {attempts > 0
                ? `Deleting failed ${attempts} time${attempts > 1 ? "s" : ""}. Try again.`
                : "All deployments, domains and environment variables will be removed. This can’t be undone."}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction
              variant="destructive"
              onClick={async () => {
                await wait(1500)
                if (shouldFail) {
                  setAttempts((count) => count + 1)
                  throw new Error("Request failed")
                }
              }}
            >
              Delete project
            </AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
      <Button
        variant="ghost"
        size="sm"
        aria-pressed={shouldFail}
        onClick={() => setShouldFail(!shouldFail)}
      >
        Request will {shouldFail ? "fail" : "succeed"}
      </Button>
    </div>
  )
}

メディア

<AlertDialogMedia /> は、デスクトップではタイトルの横に、スマートフォンではタイトルの上にアイコンを配置します。破壊的なアクションには variant="destructive" を使います。

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

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

export function AlertDialogWithMedia() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Delete file
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogMedia variant="destructive">
            <IconTrash />
          </AlertDialogMedia>
          <AlertDialogTitle>Delete “invoice-2026.pdf”?</AlertDialogTitle>
          <AlertDialogDescription>
            The file moves to the trash and is removed after 30 days.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction variant="destructive">Delete</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

小

size="sm" はコンテンツを中央揃えにし、ボタンを横並びに配置します。短い質問向けです。

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

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

export function AlertDialogSmall() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Sign out
      </AlertDialogTrigger>
      <AlertDialogContent size="sm">
        <AlertDialogHeader>
          <AlertDialogMedia>
            <IconLogout />
          </AlertDialogMedia>
          <AlertDialogTitle>Sign out?</AlertDialogTitle>
          <AlertDialogDescription>
            You’ll need to sign in again on this device.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialogAction>Sign out</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

分離したトリガー

createAlertDialogHandle() でハンドルを作成すると、1 つのダイアログを複数のトリガーで共有できます。各トリガーは独自の payload を渡し、ダイアログは render 関数を通じてそれを読み取ります。

"use client"

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

const removeMember = createAlertDialogHandle<{ name: string }>()

export function AlertDialogDetachedTriggers() {
  return (
    <div className="flex flex-wrap gap-2">
      {["Olivia", "Liam"].map((name) => (
        <AlertDialogTrigger
          key={name}
          handle={removeMember}
          payload={{ name }}
          render={<Button variant="outline" />}
        >
          Remove {name}
        </AlertDialogTrigger>
      ))}
      <AlertDialog handle={removeMember}>
        {({ payload }) => (
          <AlertDialogContent size="sm">
            <AlertDialogHeader>
              <AlertDialogTitle>Remove {payload?.name}?</AlertDialogTitle>
              <AlertDialogDescription>
                They’ll lose access to this workspace.
              </AlertDialogDescription>
            </AlertDialogHeader>
            <AlertDialogFooter>
              <AlertDialogCancel>Cancel</AlertDialogCancel>
              <AlertDialogAction variant="destructive">
                Remove
              </AlertDialogAction>
            </AlertDialogFooter>
          </AlertDialogContent>
        )}
      </AlertDialog>
    </div>
  )
}

制御

open と onOpenChange を渡すと、トリガーなしでコードから開けます。フォーカスは引き続きキャンセルに移ります。

"use client"

import * as React from "react"

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

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

  return (
    <>
      <Button
        variant="outline"
        onClick={() => window.setTimeout(() => setOpen(true), 600)}
      >
        Open in 600ms
      </Button>
      <AlertDialog open={open} onOpenChange={setOpen}>
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogTitle>Session expiring</AlertDialogTitle>
            <AlertDialogDescription>
              You’ve been inactive for a while. Stay signed in?
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>Sign out</AlertDialogCancel>
            <AlertDialogAction>Stay signed in</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  )
}

入れ子

別のダイアログの内側から開いたダイアログは、手前に重なります。子が開いている間、親は縮小して奥に下がり、子が閉じると手前に戻ります。

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

export function AlertDialogNested() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Reset settings
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Reset all settings?</AlertDialogTitle>
          <AlertDialogDescription>
            Theme, notifications and shortcuts go back to their defaults.
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Cancel</AlertDialogCancel>
          <AlertDialog>
            <AlertDialogTrigger render={<Button variant="destructive" />}>
              Reset
            </AlertDialogTrigger>
            <AlertDialogContent size="sm">
              <AlertDialogHeader>
                <AlertDialogTitle>Are you really sure?</AlertDialogTitle>
                <AlertDialogDescription>
                  The parent dialog steps back while this one is open.
                </AlertDialogDescription>
              </AlertDialogHeader>
              <AlertDialogFooter>
                <AlertDialogCancel>Go back</AlertDialogCancel>
                <AlertDialogAction variant="destructive">
                  Reset
                </AlertDialogAction>
              </AlertDialogFooter>
            </AlertDialogContent>
          </AlertDialog>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

長いコンテンツ

画面より高いコンテンツはダイアログ内でスクロールし、ページはロックされたままです。

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

const sections = Array.from({ length: 12 }, (_, index) => index + 1)

export function AlertDialogLongContent() {
  return (
    <AlertDialog>
      <AlertDialogTrigger render={<Button variant="outline" />}>
        Review terms
      </AlertDialogTrigger>
      <AlertDialogContent>
        <AlertDialogHeader>
          <AlertDialogTitle>Accept the updated terms?</AlertDialogTitle>
          <AlertDialogDescription render={<div />}>
            {sections.map((section) => (
              <p key={section}>
                Section {section}. We may update these terms from time to time.
                Continued use of the service after changes take effect means you
                accept the new terms. Read the <a href="#">full policy</a> for
                details.
              </p>
            ))}
          </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
          <AlertDialogCancel>Decline</AlertDialogCancel>
          <AlertDialogAction>Accept</AlertDialogAction>
        </AlertDialogFooter>
      </AlertDialogContent>
    </AlertDialog>
  )
}

右から左

コンテンツは RTL コンテナの外側のポータルにレンダリングされるため、コンテンツにも dir を渡してください。フッターの矢印キーは読む方向に従います。

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

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

export function AlertDialogRtl() {
  return (
    <div dir="rtl">
      <AlertDialog>
        <AlertDialogTrigger render={<Button variant="outline" />}>
          حذف الملف
        </AlertDialogTrigger>
        <AlertDialogContent dir="rtl">
          <AlertDialogHeader>
            <AlertDialogMedia variant="destructive">
              <IconTrash />
            </AlertDialogMedia>
            <AlertDialogTitle>حذف هذا الملف؟</AlertDialogTitle>
            <AlertDialogDescription>
              لا يمكن التراجع عن هذا الإجراء.
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel>إلغاء</AlertDialogCancel>
            <AlertDialogAction variant="destructive">حذف</AlertDialogAction>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </div>
  )
}
キーアクション
EnterSpaceトリガーに指定すると、ダイアログを開き、フォーカスをキャンセルに移動します。
TabShift+Tabコントロール間でフォーカスを移動します。フォーカスはダイアログ内にとどまります。
←→↑↓フッターのボタン間を移動し、端で反対側に回り込みます。左右は読む方向に従います。
Escダイアログを閉じ、フォーカスをトリガーに戻します。非同期アクションの実行中は無視されます。
  • コンテンツには role="alertdialog" が付き、タイトルによってラベル付けされ、説明によって補足されます。
  • フォーカスは最も破壊的でない選択肢である <AlertDialogCancel /> から始まります。変更するには initialFocus を渡します。
  • 背景をクリックしても閉じないため、操作の判断が誤って閉じられることはありません。許可するには disablePointerDismissal={false} を設定します。
  • スマートフォンでは、下にスワイプしてキャンセルできるボトムシートになります。アクションの保留中は、スワイプ、Esc、キャンセルがブロックされ、ボタンにはフォーカスできる状態が保たれます。
  • モーションの低減が有効な場合は、拡大縮小ではなくフェードになります。

<Sheet /> を通じて、Base UI の drawer をベースにしています。各パーツは、ラップしているプリミティブや要素の props をすべて受け付けます。

プロパティ型デフォルト
open
boolean–
defaultOpen
booleanfalse
onOpenChange保留中のアクションによって閉じるのがブロックされた場合は呼ばれません。
(open: boolean, details) => void–
onOpenChangeComplete開閉アニメーションの後に呼ばれます。
(open: boolean) => void–
disablePointerDismissal背景をクリックしてもダイアログを開いたままにします。
booleantrue
handle分離したトリガーを接続します。
AlertDialogHandle<Payload>–
actionsRefダイアログをコードから閉じる、またはアンマウントします。
RefObject<{ close, unmount }>–
children
ReactNode | ({ payload }) => ReactNode–
プロパティ型デフォルト
handle
AlertDialogHandle<Payload>–
payloadダイアログの render 関数に渡されます。
Payload–
render
ReactElement | (props, state) => ReactElement<button>
属性説明
data-slot="alert-dialog-trigger"CSSでトリガーを指定します。
data-popup-openダイアログが開いている間、付与されます。
プロパティ型デフォルト
size
"default" | "sm""default"
initialFocus
boolean | RefObject | (openType) => HTMLElement | booleanCancel button
finalFocus
boolean | RefObject | (closeType) => HTMLElement | booleanThe trigger
dirダイアログを右から左の表示にしたいときに設定します。
"ltr" | "rtl"–
render
ReactElement | (props, state) => ReactElement<div>
属性説明
data-slot="alert-dialog-content"CSS でダイアログを指定します。
data-size現在のサイズ。
data-open開いている間存在します。
data-starting-styleダイアログが開くアニメーションの間、付与されます。
data-ending-styleダイアログが閉じるアニメーションの間、付与されます。
data-nested-drawer-openネストしたダイアログが上で開いている間、付与されます。
data-swipingスマートフォンでスワイプしている間、付与されます。
--nested-drawersこのダイアログの上で開いているダイアログの数。

メディア、タイトル、説明を縦に並べる <div>。

属性説明
data-slot="alert-dialog-header"CSSでヘッダーを指定します。
プロパティ型デフォルト
variant
"default" | "destructive""default"
属性説明
data-slot="alert-dialog-media"CSS でメディアを指定します。
プロパティ型デフォルト
render
ReactElement | (props, state) => ReactElement<h2>
属性説明
data-slot="alert-dialog-title"CSSでタイトルを指定します。
プロパティ型デフォルト
render複数の段落を含める場合は render={<div />} を使います。
ReactElement | (props, state) => ReactElement<p>
属性説明
data-slot="alert-dialog-description"CSSで説明を指定します。

ボタン用の <div>。スマートフォンではボタンが全幅で縦に積まれ、矢印キーでボタン間を移動できます。

属性説明
data-slot="alert-dialog-footer"CSS でフッターを指定します。

クリックするとダイアログを閉じる <Button />。Button のすべての props を受け付けます。

プロパティ型デフォルト
onClickPromise を返すと、スピナーを表示し、完了するまでダイアログを開いたままにします。開いたままにするには event.preventDefault() を呼びます。
(event) => void | PromiseLike<unknown>–
variant
ButtonVariant"default"
属性説明
data-slot="alert-dialog-action"CSS でアクションを指定します。
プロパティ型デフォルト
variant
ButtonVariant"outline"
size
ButtonSize–
disabledアクションの保留中も無効になります。
boolean–
属性説明
data-slot="alert-dialog-cancel"CSS でキャンセルボタンを指定します。最初にフォーカスを受け取ります。
data-disabled無効な間、付与されます。

<AlertDialogContent /> はすでに両方をレンダリングします。カスタムのポップアップを組み立てるときにのみ使ってください。

プロパティ型デフォルト
keepMountedポータルに指定すると、閉じている間もダイアログを DOM に残します。
booleanfalse

ページ上のどこにある <AlertDialogTrigger /> 要素でも、1 つの <AlertDialog /> に接続するハンドルを返します。payload の型はジェネリクスで指定します: createAlertDialogHandle<{ name: string }>()。

使用しているブロック

Alert dialog の上に構築されるブロック。