No.184
MODAL CSSJS

Alert Dialog Shake

削除のように取り消せない操作の確認ダイアログで、外側を押しても閉じずに左右へ小さく揺れて「どちらかを選んでください」と知らせる演出。最初のフォーカスは安全なキャンセルに置いています。

MY COLLECTION RANDOM ↗

DEMO

アカウント設定

CODE

HTML
<div class="demo-stage ads-stage">
  <div class="ads-page" id="ads-page">
    <p class="ads-lead">アカウント設定</p>
    <button type="button" class="ads-open" id="ads-open">アカウントを削除</button>
    <p class="ads-status" id="ads-status" role="status" aria-live="polite"></p>
  </div>
  <div class="ads-backdrop" id="ads-backdrop" hidden>
    <div class="ads-dialog" id="ads-dialog" role="alertdialog" aria-modal="true" aria-labelledby="ads-title" aria-describedby="ads-desc">
      <h3 class="ads-title" id="ads-title">アカウントを削除しますか?</h3>
      <p class="ads-desc" id="ads-desc">保存したエフェクトとメモがすべて消え、元に戻せません。</p>
      <p class="ads-note" id="ads-note" aria-live="polite"></p>
      <div class="ads-actions">
        <button type="button" class="ads-cancel" id="ads-cancel">キャンセル</button>
        <button type="button" class="ads-danger" id="ads-confirm">削除する</button>
      </div>
    </div>
  </div>
</div>
CSS
.ads-stage { position:relative; overflow:hidden; display:flex; align-items:center; justify-content:center; padding:1.5rem; background:#0B0B10; min-height:320px; }
.ads-page { display:flex; flex-direction:column; align-items:center; gap:.8rem; text-align:center; }
.ads-lead { margin:0; color:#EDEDED; font-size:.95rem; font-weight:600; }
.ads-open { min-height:44px; padding:0 1.2rem; border-radius:10px; border:1px solid rgba(248,113,113,.55); background:#1C1214; color:#FCA5A5; font-weight:600; font-size:.86rem; cursor:pointer; }
.ads-status { margin:0; min-height:1.2em; color:#A1A1AA; font-size:.78rem; }
.ads-backdrop { position:absolute; inset:0; z-index:5; display:flex; align-items:center; justify-content:center; padding:1rem; background:rgba(0,0,0,.6); animation: ads-fade .2s ease; }
.ads-backdrop[hidden] { display:none; }
.ads-dialog { width:100%; max-width:300px; box-sizing:border-box; padding:1.2rem; border-radius:14px; background:#1E1E26; border:1px solid rgba(255,255,255,.12); box-shadow:0 20px 50px rgba(0,0,0,.5); animation: ads-pop .22s cubic-bezier(.22,1,.36,1); }
.ads-title { margin:0 0 .4rem; color:#fff; font-size:1rem; }
.ads-desc { margin:0; color:#C9C9D1; font-size:.8rem; line-height:1.7; }
.ads-note { margin:.5rem 0 0; min-height:1.2em; color:#FCA5A5; font-size:.74rem; line-height:1.6; }
.ads-actions { display:flex; justify-content:flex-end; gap:.5rem; margin-top:.9rem; }
.ads-cancel { min-height:44px; padding:0 1rem; border-radius:10px; border:1px solid rgba(255,255,255,.22); background:transparent; color:#EDEDED; font-size:.84rem; cursor:pointer; }
.ads-danger { min-height:44px; padding:0 1rem; border:0; border-radius:10px; background:#B91C1C; color:#fff; font-weight:600; font-size:.84rem; cursor:pointer; }
.ads-open:focus-visible, .ads-cancel:focus-visible, .ads-danger:focus-visible { outline:2px solid #fff; outline-offset:3px; }
@keyframes ads-fade { from { opacity:0; } }
@keyframes ads-pop { from { opacity:0; transform:scale(.95); } }
@media (prefers-reduced-motion: reduce) {
  .ads-backdrop, .ads-dialog { animation:none; }
  .ads-dialog.is-flash { outline:2px solid #FCA5A5; outline-offset:2px; }
}
JS
(function(){
  var page = document.getElementById('ads-page');
  var openBtn = document.getElementById('ads-open');
  var back = document.getElementById('ads-backdrop');
  var dlg = document.getElementById('ads-dialog');
  var cancel = document.getElementById('ads-cancel');
  var ok = document.getElementById('ads-confirm');
  var note = document.getElementById('ads-note');
  var status = document.getElementById('ads-status');
  if (!page || !openBtn || !back || !dlg || !cancel || !ok || !note || !status) return;
  var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
  var flashTimer = 0;
  var SHAKE = [0, -2, 4, -8, 8, -8, 8, -8, 4, -2, 0].map(function(x){ return { transform: 'translateX(' + x + 'px)' }; });
  function open(){
    back.hidden = false; page.inert = true;
    note.textContent = '';
    cancel.focus({ preventScroll: true });
  }
  function close(msg){
    back.hidden = true; page.inert = false;
    clearTimeout(flashTimer); dlg.classList.remove('is-flash');
    status.textContent = msg;
    openBtn.focus({ preventScroll: true });
  }
  function shake(){
    if (reduce.matches || !dlg.animate) {
      dlg.classList.add('is-flash');
      clearTimeout(flashTimer);
      flashTimer = setTimeout(function(){ dlg.classList.remove('is-flash'); }, 450);
    } else {
      dlg.animate(SHAKE, { duration: 400, easing: 'cubic-bezier(.36,.07,.19,.97)' });
    }
    note.textContent = '外側を押しても閉じません。どちらかのボタンを選んでください';
  }
  openBtn.addEventListener('click', open);
  cancel.addEventListener('click', function(){ close('削除を取り消しました'); });
  ok.addEventListener('click', function(){ close('アカウントを削除しました(デモのため実際には削除されません)'); });
  back.addEventListener('mousedown', function(e){ if (!e.target.closest('button')) e.preventDefault(); });
  back.addEventListener('click', function(e){ if (e.target === back) shake(); });
  document.addEventListener('keydown', function(e){
    if (back.hidden) return;
    if (e.key === 'Escape') { e.preventDefault(); close('削除を取り消しました'); return; }
    if (e.key !== 'Tab') return;
    if (!dlg.contains(document.activeElement)) { e.preventDefault(); cancel.focus(); }
    else if (e.shiftKey && document.activeElement === cancel) { e.preventDefault(); ok.focus(); }
    else if (!e.shiftKey && document.activeElement === ok) { e.preventDefault(); cancel.focus(); }
  });
})();

AI PROMPT

Claude Code Cursor v0
PROMPT
削除のように取り消せない操作の確認ダイアログで、外側を押しても閉じずに左右へ小さく揺れて知らせる演出を作って。ダイアログは role="alertdialog"・aria-modal="true"・aria-labelledby(見出し)・aria-describedby(説明文)を付け、半透明の背景(position:absolute; inset:0)の中央に置く。開いたら後ろの内容を inert にし、最初のフォーカスは安全な「キャンセル」ボタンに置く(取り消せない操作なので、Enter を押しただけで実行されないようにする)。背景そのもの(ダイアログの外側)を押した時は閉じずに、ダイアログを Web Animations API の element.animate で translateX 0 → −2px → 4px → −8px → 8px → −8px → 8px → −8px → 4px → −2px → 0 と .4秒揺らし(CSS のクラスを付け外しすると、出現の animation が再生し直されてしまうため)、背景とダイアログ内のボタン以外の mousedown は preventDefault してフォーカスが抜けないようにし(キー操作は開いている間だけ document で受ける)、「外側を押しても閉じません。どちらかのボタンを選んでください」と表示する(aria-live で伝える)。Esc はキャンセルと同じ扱いで閉じる。Tab と Shift+Tab は2つのボタンの間だけを回る。閉じたら開いたボタンへフォーカスを戻し、結果を role="status" の段落で伝える。「削除する」ボタンは白い文字とのコントラストが4.5:1以上ある濃い赤にする。デモでは枠の中に作るため position:absolute を使う(ページ全体に使う場合は dialog 要素の showModal() を使う方法も紹介する)。@media (prefers-reduced-motion: reduce) では揺らさず、ダイアログの縁取りを0.45秒だけ赤くして知らせる。

Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift

AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する

BOOKS · PR · AMAZON

📖 なかしまぁ先生のCSSアニメーション入門 📖 UIデザインの心理学 ―わかりやすさ・使いやすさの法則 📖 Fundamentals of Web Animation with GSAP

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

学びを止めない · AMAZONサブスク

📚 Kindle Unlimited — 技術書・デザイン書が読み放題。30日無料体験 → 🎧 Audible — 移動中に耳で学ぶオーディオブック。30日無料体験 → 📦 Amazon Prime — Prime Reading・配送特典つき。30日無料体験 → 🎵 Music Unlimited — 作業用BGMに。1億曲が聴き放題・30日無料体験 →

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

CREATOR TOOLS · スクール(PR)

🎬 Adobe Creative Cloud — After Effects / Photoshop 業界標準ツール → 💻 SkillHacks — プログラミング・動画編集オンラインスクール → 🚀 デイトラ — Web制作スクール・実案件レベルまで習得 →

※本サイトはアフィリエイトリンクを含みます。リンク経由のご購入・お申し込みで運営者が紹介料を受け取る場合があります。

RELATED EFFECTS

↗ MORE
→ MODAL SCALE BLUR → MODAL FADE → TOAST NOTIFICATION

Alert Dialog Shake(揺れて知らせる確認ダイアログ)とは

アカウントの削除のように、取り消せない操作の前に出す確認ダイアログで、外側を押した時に閉じる代わりにダイアログを左右へ小さく揺らす演出です。首を横に振るような小さな揺れで、「まだ選んでいない」ことを動きで伝えます。

当サイトの Modal Scale Blur は背景をぼかしてモーダルが弾性スケールで開くダイアログ、Modal Fade はオーバーレイがフェードしダイアログがスケールインするモーダル、Toast Notification は右上からスライドインして自動で消える通知です。本ページは確認を求める場面に絞り、外側を押した時の応え方と最初のフォーカスの置き場所を扱う別テーマです。

揺れは注意を引く強い動きなので、本当に選んでもらう必要がある場面だけに使ってください。動きを減らす設定の利用者には揺らさず、縁取りの色と文章で同じことを伝えれば、意味はそのまま伝わります。

よくある質問

外側を押しても閉じないのはなぜですか?

WAI-ARIA Authoring Practices Guide の説明では、アラートダイアログは利用者の作業を中断して重要なメッセージを伝え、応答を求めるダイアログで、操作の確認などに使われます。取り消せない操作の確認では、外側を押しただけで閉じると、選んだつもりがないまま先へ進んだり、何が起きたか分からなくなったりします。本実装は外側を押しても閉じず、揺れと文章で「ボタンで選んでください」と知らせ、Esc はキャンセルとして扱っています。

ページ全体に使うなら dialog 要素のほうがよいですか?

はい。dialog 要素を showModal() で開くと、後ろの内容が自動で操作できなくなり、背景は ::backdrop で装飾できます。MDN によると、closedby 属性を指定しない場合、showModal() で開いたダイアログは Esc などの閉じる要求では閉じ、外側を押しても閉じない動作になります。MDN の互換性データでは showModal() は Chrome 37・Edge 79・Firefox 98・Safari 15.4 から使えるとされています。本ページはデモの枠の中で完結させるため、同じ動きを div と inert で作っています。

コードは無料で使えますか?

はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。