デザインの様式を、動くデモ・効く理由・実装で踏んだ罠つきで解剖する図鑑。
様式名: ソフト・グラデーション / モーダル
「壊れた例」を押すと、この様式が成立しなくなる条件を実物で見られます。
| 対象 | 文字色 | 背景 | 実効背景 | 比 | 判定 |
|---|---|---|---|---|---|
| 見出し(淡グラデ帯・明部) | #1e293b | #fde7f0 | #fde7f0 | 12.44:1 | AA 合格 |
| 本文(白) | #3f4a5c | #ffffff | #ffffff | 8.95:1 | AA 合格 |
| 削除ボタン 白文字(赤地) | #ffffff | #b42318 | #b42318 | 6.57:1 | AA 合格 |
| ゴースト枠(非文字3:1基準・白地) | #8593a8 | #ffffff | #ffffff | 3.12:1 | AA 合格 |
| 背景説明文(淡グラデ地) | #5b6676 | #f7eef6 | #f7eef6 | 5.13:1 | AA 合格 |
| 壊れた例:薄い×文字 | #94a3b8 | #f4e9f2 | #f4e9f2 | 2.17:1 | 不合格 |
<!doctype html>
<html lang="ja"><head><meta charset="utf-8"><title>モーダルダイアログ デモ</title><style>
*{box-sizing:border-box;margin:0;padding:0}
:root{--ink:#1e293b; --body:#3f4a5c; --sub:#5b6676; --line:#8593a8;
--accent:#4338ca; --danger:#b42318;}
body{min-height:340px;padding:26px 24px;color:var(--ink);
font-family:system-ui,-apple-system,"Hiragino Kaku Gothic ProN","Yu Gothic",sans-serif;
/* ソフト・グラデーション:淡い地 */
background:linear-gradient(135deg,#eef2fb 0%,#f7eef6 50%,#eef6f4 100%)}
.page{max-width:560px;margin:0 auto}
.page h3{font-size:17px;font-weight:700}
.page p{font-size:13px;color:var(--sub);margin:6px 0 16px;line-height:1.7}
.open-btn{font:inherit;font-size:13px;font-weight:700;color:#fff;background:var(--danger);
border:0;border-radius:9px;padding:11px 18px;min-height:44px;cursor:pointer}
.open-btn:focus-visible{outline:3px solid var(--danger);outline-offset:2px}
/* 背景スクリム:ページを暗く落として奥行きと集中を作る */
.scrim{position:fixed;inset:0;background:rgba(15,23,42,.55);
display:flex;align-items:center;justify-content:center;padding:20px}
.scrim[hidden]{display:none}
/* モーダル本体:淡いグラデ見出し帯+白い本文。文字は必ず濃色 */
.modal{width:100%;max-width:400px;background:#fff;border-radius:16px;overflow:hidden;
box-shadow:0 24px 60px rgba(15,23,42,.4)}
.modal .bar{background:linear-gradient(120deg,#fde7f0,#e8edfb);padding:18px 22px;
display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
.modal .bar h2{font-size:16px;font-weight:700;color:var(--ink);line-height:1.4}
/* 閉じるは色だけの × でなく、aria-label 付きの本物の button */
.x{flex:none;width:32px;height:32px;display:grid;place-items:center;border:0;border-radius:8px;
background:rgba(255,255,255,.6);color:var(--ink);cursor:pointer}
.x:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
.modal .bd{padding:18px 22px 8px;font-size:13px;line-height:1.85;color:var(--body)}
.acts{display:flex;gap:10px;justify-content:flex-end;padding:14px 22px 20px}
.btn{font:inherit;font-size:13px;font-weight:700;border-radius:9px;padding:10px 16px;
min-height:44px;cursor:pointer}
.btn.ghost{background:#fff;border:1.5px solid var(--line);color:var(--ink)}
.btn.danger{background:var(--danger);border:1.5px solid var(--danger);color:#fff}
.btn:focus-visible{outline:3px solid var(--accent);outline-offset:2px}
</style></head><body>
<div class="page">
<h3>アカウント設定</h3>
<p>下のボタンでダイアログを開きます。開くとフォーカスがダイアログ内に移り、Tab は内側だけを巡回します。Esc キーか背景クリックで閉じ、フォーカスはボタンに戻ります。</p>
<button class="open-btn" id="opener">アカウントを削除</button>
</div>
<div class="scrim" id="scrim" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle" aria-describedby="mDesc">
<div class="bar">
<h2 id="mTitle">本当にアカウントを削除しますか?</h2>
<button class="x" id="closeX" aria-label="閉じる">
<svg width="15" height="15" viewBox="0 0 15 15" aria-hidden="true"><path stroke="currentColor" stroke-width="1.8" stroke-linecap="round" d="M3 3l9 9M12 3l-9 9"/></svg>
</button>
</div>
<div class="bd" id="mDesc">この操作は取り消せません。作成したプロジェクトと設定はすべて失われ、復元できません。続けてよろしいですか?</div>
<div class="acts">
<button class="btn ghost" id="cancelBtn">キャンセル</button>
<button class="btn danger" id="confirmBtn">削除する</button>
</div>
</div>
</div>
<script>
const scrim=document.getElementById('scrim'), opener=document.getElementById('opener');
const modal=scrim.querySelector('.modal');
let lastFocus=null;
function focusables(){return modal.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])');}
function open(){
lastFocus=document.activeElement;
scrim.hidden=false;
document.body.style.overflow='hidden'; // 開いている間は背後のスクロールを止める
document.getElementById('cancelBtn').focus();
document.addEventListener('keydown',onKey);
}
function close(){
scrim.hidden=true;
document.body.style.overflow=''; // スクロールロックを解除
document.removeEventListener('keydown',onKey);
if(lastFocus) lastFocus.focus(); // フォーカスをトリガに戻す
}
function onKey(e){
if(e.key==='Escape'){ close(); return; }
if(e.key==='Tab'){ // フォーカストラップ
const f=focusables(), first=f[0], last=f[f.length-1];
if(e.shiftKey && document.activeElement===first){ e.preventDefault(); last.focus(); }
else if(!e.shiftKey && document.activeElement===last){ e.preventDefault(); first.focus(); }
}
}
opener.addEventListener('click',open);
document.getElementById('closeX').addEventListener('click',close);
document.getElementById('cancelBtn').addEventListener('click',close);
document.getElementById('confirmBtn').addEventListener('click',close);
scrim.addEventListener('click',e=>{ if(e.target===scrim) close(); }); // 背景クリックで閉じる
open(); // デモは開いた状態で表示
</script>
</body></html>