ダイアログ
確認ダイアログ
元に戻しにくい操作の対象と影響を説明し、取消、処理中、失敗、完了までを安全に扱う確認レシピです。
動きを試す
プレビュー
必要なファイルを選ぶ
コード
index.html
利用前に変更する箇所
- 対象名、影響、実行文言を実装先の操作へ置き換える
<!doctype html>
<html lang="ja" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>確認ダイアログ</title>
<link rel="stylesheet" href="./styles.css" />
<script src="./dialog.js" defer></script>
</head>
<body>
<main>
<section class="demo-card" aria-labelledby="page-title">
<p class="eyebrow">確認操作の表示例</p>
<h1 id="page-title">保存前の下書き</h1>
<p class="lead">取り消しに手間がかかる操作だけを、対象と影響が分かる確認画面につなぎます。</p>
<div class="draft-summary">
<strong>週次レポートの下書き</strong>
<span>最終編集: 10分前</span>
</div>
<button class="danger-trigger" type="button" data-open-dialog>下書きを破棄する</button>
<p class="result" role="status" aria-live="polite" data-result></p>
</section>
<dialog aria-labelledby="dialog-title" aria-describedby="dialog-description" data-dialog>
<form method="dialog" class="dialog-card">
<p class="eyebrow">実行前の確認</p>
<h2 id="dialog-title">この下書きを破棄しますか?</h2>
<p id="dialog-description">
「週次レポートの下書き」に保存していない変更があります。破棄すると、この画面からは戻せません。
</p>
<div class="error-message" role="alert" data-error hidden>
<strong>破棄を完了できませんでした。</strong>
<span>通信は行っていません。もう一度試すか、取り消して内容を確認してください。</span>
</div>
<p class="processing-message" role="status" data-processing hidden>破棄処理を確認しています…</p>
<div class="dialog-actions">
<button type="submit" value="cancel" data-cancel>取り消す</button>
<button class="danger-action" type="button" data-confirm>破棄する</button>
</div>
</form>
</dialog>
</main>
</body>
</html>
styles.css
利用前に変更する箇所
- 色tokenと余白を実装先のデザインシステムへ接続する
:root {
color-scheme: light;
--bg: #f4f7f8;
--surface: #ffffff;
--surface-subtle: #e9f0f2;
--text: #152129;
--muted: #53656f;
--border: #c6d3d8;
--danger: #a52a3a;
--danger-strong: #7f1625;
--danger-surface: #fff0f1;
--focus: #007c92;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans JP", sans-serif;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #0b1117;
--surface: #111a21;
--surface-subtle: #19252e;
--text: #edf5f7;
--muted: #a8b8c0;
--border: #33434e;
--danger: #ff8794;
--danger-strong: #ffb5bd;
--danger-surface: #351820;
--focus: #5adaf2;
}
* {
box-sizing: border-box;
}
body {
min-width: 0;
margin: 0;
padding: clamp(12px, 4vw, 28px);
color: var(--text);
background: var(--bg);
font-size: 16px;
line-height: 1.6;
}
main {
width: min(100%, 680px);
margin-inline: auto;
}
.demo-card,
.dialog-card {
min-width: 0;
padding: clamp(16px, 4vw, 28px);
border: 1px solid var(--border);
border-radius: 12px;
background: var(--surface);
}
.eyebrow {
margin: 0 0 4px;
color: var(--muted);
font-size: 0.875rem;
font-weight: 800;
}
h1,
h2 {
margin: 0;
line-height: 1.35;
overflow-wrap: anywhere;
}
h1 {
font-size: clamp(1.4rem, 5vw, 2rem);
}
h2 {
font-size: clamp(1.25rem, 5vw, 1.6rem);
}
.lead,
#dialog-description {
margin: 8px 0 20px;
color: var(--muted);
}
.draft-summary {
display: grid;
gap: 2px;
margin-bottom: 18px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface-subtle);
}
.draft-summary span {
color: var(--muted);
font-size: 0.875rem;
}
button {
min-height: 44px;
padding: 9px 14px;
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
background: var(--surface);
font: inherit;
font-weight: 800;
cursor: pointer;
}
button:hover:not(:disabled) {
border-color: currentColor;
}
button:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 3px;
}
button:disabled {
cursor: wait;
opacity: 0.62;
}
.danger-trigger {
color: var(--danger-strong);
}
.danger-action {
border-color: var(--danger);
color: #fff;
background: var(--danger-strong);
}
:root[data-theme="dark"] .danger-action {
color: #1b0910;
background: var(--danger);
}
.result {
min-height: 1.6em;
margin: 14px 0 0;
color: var(--muted);
}
dialog {
width: min(calc(100% - 24px), 560px);
max-height: calc(100dvh - 24px);
padding: 0;
border: 0;
color: var(--text);
background: transparent;
}
dialog::backdrop {
background: rgb(5 12 17 / 72%);
}
.dialog-card {
overflow: auto;
}
.dialog-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
margin-top: 22px;
}
.error-message {
display: grid;
gap: 3px;
margin-top: 16px;
padding: 12px;
border-left: 4px solid var(--danger);
background: var(--danger-surface);
overflow-wrap: anywhere;
}
.processing-message {
margin: 16px 0 0;
color: var(--muted);
font-weight: 700;
}
[hidden] {
display: none;
}
@media (max-width: 420px) {
body {
padding: 10px;
}
.demo-card,
.dialog-card {
padding: 16px;
}
.dialog-actions {
display: grid;
}
.dialog-actions button {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto;
transition-duration: 0.01ms;
}
}
dialog.js
利用前に変更する箇所
- 固定timerを実処理へ置き換え、失敗時もdialog内で回復できるよう接続する
(() => {
const params = new URLSearchParams(window.location.search);
const instance = params.get("instance") ?? "missing-instance";
const validStates = new Set(["closed", "open", "processing", "action-error", "completed"]);
const validThemes = new Set(["light", "dark"]);
const dialog = document.querySelector("[data-dialog]");
const openButton = document.querySelector("[data-open-dialog]");
const cancelButton = document.querySelector("[data-cancel]");
const confirmButton = document.querySelector("[data-confirm]");
const errorMessage = document.querySelector("[data-error]");
const processingMessage = document.querySelector("[data-processing]");
const result = document.querySelector("[data-result]");
let currentState = validStates.has(params.get("state")) ? params.get("state") : "closed";
let activeTimer;
function cancelTimer() {
if (activeTimer !== undefined) {
window.clearTimeout(activeTimer);
activeTimer = undefined;
}
}
function setTheme(theme) {
if (theme && validThemes.has(theme)) document.documentElement.dataset.theme = theme;
}
function ensureOpen() {
if (dialog instanceof HTMLDialogElement && !dialog.open) {
dialog.showModal();
}
}
function setState(nextState, shouldFocus = false) {
if (!nextState || !validStates.has(nextState)) return;
cancelTimer();
currentState = nextState;
document.documentElement.dataset.state = currentState;
const isProcessing = currentState === "processing";
if (errorMessage instanceof HTMLElement) errorMessage.hidden = currentState !== "action-error";
if (processingMessage instanceof HTMLElement) processingMessage.hidden = !isProcessing;
if (confirmButton instanceof HTMLButtonElement) {
confirmButton.disabled = isProcessing;
confirmButton.textContent =
currentState === "action-error" ? "もう一度試す" : isProcessing ? "処理中…" : "破棄する";
}
if (currentState === "closed" || currentState === "completed") {
if (dialog instanceof HTMLDialogElement && dialog.open) dialog.close(currentState);
if (result instanceof HTMLElement) {
result.textContent =
currentState === "completed"
? "下書きを破棄しました。このデモでは実際のデータは変更していません。"
: "";
}
return;
}
ensureOpen();
if (result instanceof HTMLElement) result.textContent = "";
if (shouldFocus && cancelButton instanceof HTMLButtonElement) cancelButton.focus();
}
function completeAction() {
setState("processing");
activeTimer = window.setTimeout(() => setState("completed"), 350);
}
openButton?.addEventListener("click", () => setState("open", true));
confirmButton?.addEventListener("click", completeAction);
dialog?.addEventListener("cancel", () => {
cancelTimer();
currentState = "closed";
document.documentElement.dataset.state = currentState;
});
dialog?.addEventListener("close", () => {
cancelTimer();
if (currentState !== "completed") currentState = "closed";
document.documentElement.dataset.state = currentState;
if (openButton instanceof HTMLButtonElement) openButton.focus();
});
window.addEventListener("message", (event) => {
if (event.source !== window.parent || typeof event.data !== "object" || event.data === null)
return;
const data = event.data;
if (data.source !== "wpl-host" || data.instance !== instance) return;
if (data.type === "wpl-preview-state")
setState(typeof data.value === "string" ? data.value : null, true);
if (data.type === "wpl-preview-theme")
setTheme(typeof data.value === "string" ? data.value : null);
});
setTheme(validThemes.has(params.get("theme")) ? params.get("theme") : "light");
setState(currentState, currentState !== "closed" && currentState !== "completed");
window.parent.postMessage({ source: "wpl-preview", type: "wpl-preview-ready", instance }, "*");
})();
プレビューで試せる動き
- 閉じた状態: triggerと対象だけを表示します。
- 確認中: 取消と実行を選べるmodal状態です。
- 処理中: 多重実行を防ぎながら処理状況を示します。
- 実行失敗: 失敗理由と再試行・取消をdialog内に示します。
- 完了: dialogを閉じ、実行結果を元画面に示します。
使い分けを確認する
このレシピが合う場面
適する場面
- 保存していない入力の破棄
- 取り消しに手間がかかる変更の最終確認
- 対象名と影響を明示して誤操作を減らす操作
適さない場面
- 通常の保存や頻繁な軽微操作
- 長い入力フォームや複数段階の手続き
- 単なるお知らせ
