GODRICH

【コピペOK】CSS モーダル アニメーション 9選(dialog 対応)

css モーダルは、ボタンを押した瞬間に画面の上に浮かぶ確認・選択用の小さなパネルのことで、CSS だけで開閉のアニメーションまで作れます。この記事では9種類を選び、下のグリッドは2秒ごとに自動で開閉するので、クリックはできず眺めるだけになりますが、気に入ったものは zip

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並び順は効果の強さではなく、実装の土台がどこにあるかを基準にしています。最初の4つ(01〜04)は同じ position: fixed の型をベースに、動く方向と背景の処理だけを変えた配置違いです。次の2つ(05・06)はブラウザ標準の <dialog> と popover 属性を使い、開閉のためだけに JavaScript を数行呼び出せば済みます。最後の3つ(07〜09)は状態が絡む応用形で、ボタン自体が広がったり、閉じられないモーダルが揺れたり、モーダルが重なったりします。9つとも完成した CSS ファイルと React 版は zip に入っています。

01フェード+拡大

確認や通知のような基本のモーダルは、画面の中央で少し拡大しながら現れます。閉じるときは同じ transform を逆再生するだけで、開く用と閉じる用にキーフレームを2つ書く必要はありません。

200ms0.9→1페이드만
.modal {
  position: fixed; left: 50%; top: 50%;
  transform: translate(-50%, -50%) scale(.92);
  opacity: 0; pointer-events: none;
  transition: opacity 220ms $ease-spring,
              transform 220ms $ease-spring;
}

.stage.is-open .overlay { opacity: 1; pointer-events: auto; }
.stage.is-open .modal {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
  pointer-events: auto;
}

02ボトムシート

配送方法などの選択肢は、画面下からシートがそのまま持ち上がって現れます。position: fixed と bottom: 0 を組み合わせ、transform だけを動かすので、開閉のたびに高さを計算し直す必要がありません。

280ms반만 열기손잡이
.sheet {
  position: fixed; left: 50%; bottom: 0; top: auto;
  transform: translate(-50%, 100%);
  transition: transform 300ms $ease-out;
}

.stage.is-open .sheet {
  transform: translate(-50%, 0);
  pointer-events: auto;
}

03サイドドロワー

カートや設定画面は、右端からパネルが押し出されるように現れます。overlay の不透明度とパネルの位置を同じ長さで動かすので、背景が暗くなる速さとパネルが到着する速さがそろいます。

320px왼쪽에서전체 폭
.drawer {
  position: fixed; right: 0; top: 0; bottom: 0; left: auto;
  transform: translateX(100%);
  width: min(78vw, 260px);
  transition: transform 600ms $ease-spring;
}

.stage.is-open .drawer {
  transform: translateX(0);
  pointer-events: auto;
}

04背景ぼかし

決済確認のように失敗が許されない場面では、背景がぼやけていくことで視線をモーダルへ集めます。opacity だけでなく backdrop-filter の blur 値もあわせて動かすのが要点です。

8px어둡게만12px
.overlay {
  position: fixed; inset: 0;
  background: rgba($stage-ink, .28);
  backdrop-filter: blur(0px);
  opacity: 0; pointer-events: none;
  transition: opacity 600ms $ease-out,
              backdrop-filter 600ms $ease-out;
}

.stage.is-open .overlay {
  opacity: 1;
  backdrop-filter: blur(8px);
  pointer-events: auto;
}

05ネイティブ dialog

<dialog> をそのまま @starting-style で開けば、フォーカストラップと ESC を自前で書かずに、CSS だけで開閉のアニメーションが付きます。overlay 用の背景と ::backdrop が二重にならないよう、::backdrop は透明にしてあります。

@starting-styleallow-discrete::backdrop
dialog.modal::backdrop { background: transparent; }

dialog.modal {
  opacity: 0;
  transform: scale(.92) translateY(10px);
  transition: opacity 300ms $ease-out,
              transform 300ms $ease-out,
              overlay 300ms allow-discrete,
              display 300ms allow-discrete;
  &[open] { opacity: 1; transform: none; }
  @starting-style {
    &[open] { opacity: 0; transform: scale(.92) translateY(10px); }
  }
}

06ポップオーバー確認

削除確認のような小さな確認窓は、ボタンのすぐ上に anchor で貼り付けます。popover 属性が付く前は普通の position: absolute の箱として描き、実際に開くと :popover-open が座標を top layer 用に置き換えます。

anchor위쪽화살표 없이
.confirm {
  position: absolute; left: 50%; bottom: calc(100% + 10px);
  transform: translateX(-50%) translateY(6px) scale(.92);
  opacity: 0; pointer-events: none;
  transition: opacity 220ms $ease-pop,
              transform 220ms $ease-pop;
}

.confirm:popover-open {
  position: fixed; inset: auto;
  opacity: 1;
  transform: translateX(-50%) translateY(0) scale(1);
}

07ボタンから全画面

押したサムネイルそのものが画面いっぱいまで拡大し、そのまま全画面モーダルになります。width や height ではなく transform: scale だけを動かすので、拡大している間もレイアウトの再計算が起きません。

420ms둥글게 유지페이드 섞기
.thumb {
  position: fixed; inset: 0;
  transform: translate(-30%, 20%) scale(.3);
  transform-origin: 20% 75%;
  transition: transform 600ms $ease-spring,
              border-radius 600ms $ease-spring;
}

.stage.is-open .thumb {
  transform: translate(0, 0) scale(1);
  border-radius: 0;
}

08外側クリックで揺れる

規約同意のような閉じられないモーダルでは、外側を押しても閉じずに横へ小さく揺れて知らせます。transform の translate を左右に往復させる keyframes だけで作り、閉じる処理自体は呼びません。

4px두 번테두리 강조
.modal.is-shaking {
  animation: shake 300ms cubic-bezier(.36, .07, .19, .97) both;
  border-color: $error;
}

@keyframes shake {
  10%, 90% { transform: translate(calc(-50% - 3px), -50%); }
  20%, 80% { transform: translate(calc(-50% + 4px), -50%); border-color: $error; }
  30%, 70% { transform: translate(calc(-50% - 4px), -50%); }
  40%, 60% { transform: translate(calc(-50% + 3px), -50%); }
  50%      { transform: translate(calc(-50% - 3px), -50%); border-color: $error; }
}

09重なるモーダル

多段フォームで2つ目のモーダルを開くと、1つ目は少し縮んで奥へ下がり、2つ目が手前に重なって現れます。動くのはどちらも transform と opacity だけなので、重なる枚数が増えても仕組みは変わりません。

0.95어둡게3단
.stage.is-stacked .modal--1 {
  transform: translate(-50%, calc(-50% - 16px)) scale(.95);
  opacity: .55;
}
.stage.is-stacked .modal--2 {
  transform: translate(-50%, -50%) translateY(0) scale(1);
  opacity: 1;
  pointer-events: auto;
}

.modal--1, .modal--2 {
  transition: transform 600ms $ease-spring,
              opacity 600ms $ease-spring;
}

どこで崩れるか — 落とし穴

9つのうち動きが一番怪しいのは05のネイティブ dialog です。<dialog> は showModal() を呼ぶとブラウザが自前の ::backdrop を top layer に生成するため、CSS で書いた .overlay をそのまま残すと背景が二重に暗くなります。今回のファイルでは dialog.modal::backdrop { background: transparent; } として自前の overlay だけを見せていますが、この1行を忘れると開くたびに背景の濃さが変わって見えます。

06のポップオーバー確認でも似たことが起きます。popover 属性が付く前の .confirm は position: absolute でボタンの真上に置いていますが、実際に showPopover() を呼ぶと要素は top layer に移動し、座標の基準が変わります。:popover-open の状態だけ位置を fixed に切り替えていなかったときは、実機で確認すると確認窓がボタンから離れた場所に飛んで出ました。

3つ目はブラウザの対応状況です。<dialog> 要素自体は Chrome 37 以降・Safari 15.4 以降・Firefox 98 以降が対応済みですが(MDN の dialog ページ)、popover 属性は Chrome 114 以降・Safari 17 以降・Firefox 125 以降(MDN の Popover API)、@starting-style は Chrome 117 以降・Safari 17.5 以降・Firefox 129 以降でしか動きません(MDN の @starting-style)。zip の05と06のファイルは @supports で分岐させていないので、対応していないブラウザではアニメーションなしでそのまま開閉するだけになります。

修正した9本のファイルを開くパスワードは udnptj2b です。下のカードから zip を取ってください。

アクセシビリティ

9つとも prefers-reduced-motion: reduce では animation と transition の大部分を止め、開閉は一瞬で切り替わるだけになります。ただし08の「外側クリックで揺れる」だけは例外で、揺れそのものは消しても border-color: $error による赤い枠だけは残し、閉じられないという情報自体は動きがなくても伝わるようにしてあります。09の重なるモーダルは、動きを止めても手前と奥それぞれの scale と opacity の最終値はそのまま残るので、どちらが今の操作対象かは静止していても分かります。

@media (prefers-reduced-motion: reduce) {
  .overlay, .modal, .sheet, .drawer { transition-duration: 1ms; }
  .stage.is-demo .modal,
  .modal.is-shaking { animation: none; }
  .modal.is-shaking { border-color: $error; }
}

モーダル以外の CSS だけで動くパターンは CSS 分野のカテゴリー一覧 にまとめてあり、このサイトの運営情報は サイト概要ページ から見られます。

FAQ

9つのモーダルを同じページに全部置いても大丈夫ですか?

置けます。ただし05のネイティブ dialog と06の popover はどちらも開くとブラウザの top layer に乗るため、複数のダイアログを同時に開けるようにする場合は重なり順を個別に確認してください。01〜04・07〜09のように transform だけで作る自前のモーダルは z-index さえそろえておけば、いくつ並べても崩れません。

React ではクラスの付け外しの代わりに何を使いますか?

zip の react/ フォルダのコンポーネントは、CSS のクラス名を付け外しする代わりに、開いているかどうかを表す state をひとつだけ持たせています。05のネイティブ dialog だけは <dialog> 要素を参照して showModal() と close() を直接呼び分ける処理が入っています。

スマホでも同じように開閉しますか?

はい、動きます。02のボトムシートと03のサイドドロワーはどちらも transform だけで動くので、タッチでボタンを押した瞬間から同じ速さで滑ります。ただしスマホはアドレスバーの表示・非表示でビューポートの高さが変わる端末があるため、02の max-height: 62% は機種によって数ピクセルずれることがあります。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。