CSS ドロップダウン メニュー アニメーション 9選 — JS最小・CSSだけ
css ドロップダウン メニューは、クリックやホバーで隠れていた選択肢を高さや透明度の変化で見せる、CSSだけで動くナビゲーション部品です。この記事では9種類を選び、grid-template-rows・clip-path・popover属性という異なる仕組みで開閉を実装しました。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 height:auto遷移
- 02 フェード+スライド
- 03 角を起点に拡大
- 04 クリップパスで開く
- 05 項目が順番に表示
- 06 メガメニュー背景の塗りつぶし
- 07 遅延して開く(transition-delay)
- 08 popover属性を使用
- 09 モバイル全画面シート
並び順は効果の派手さではなく、開閉を作る仕組みの難易度です。まずheight:autoをgrid-template-rowsで擬似的に再現する基本形から始め(01)、それを不透明度と縦の移動に置き換え(02)、拡大の起点をトリガー側の角に寄せ(03)、clip-pathで切り出す形に変え(04)、項目ごとに時差をつける発展形を重ね(05)、最後に背景の塗りつぶし・ホバー遅延・popover属性・モバイル全画面シートという実際の場面への転用を並べました(06〜09)。9つとも実際の高さや座標の数値を直接書き換えるのではなく、grid-template-rows・opacity・transform・clip-pathという補間できる値だけを動かしています。
01height:auto遷移
パネルの.menu__panelがgrid-template-rowsを0frから1frへ0.3秒かけて動かし、内側の.menu__listにoverflow: hiddenを付けることで、height:autoを直接アニメーションできないCSSの制約を回避しています。ナビゲーションのドロップダウンや、フィルターと兼用するアコーディオンに向いています。
.menu__panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows .3s ease-out;
margin-top: 8px;
}
.menu__list {
overflow: hidden;
min-height: 0;
}
.menu.is-open .menu__panel {
grid-template-rows: 1fr;
}
02フェード+スライド
パネルが不透明度0から1へ変わると同時に、8px上の位置から下の定位置へ0.22秒かけて滑り込み、唐突に現れる印象を和らげます。ヘッダーのアカウントメニューや、上部ナビのドロップダウンに向いています。
.menu__panel {
opacity: 0;
transform: translateY(-8px);
pointer-events: none;
}
.menu.is-open .menu__panel {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
transition: opacity .22s ease-out, transform .22s ease-out;
}
03角を起点に拡大
transform-originをトリガーに近い右上の角に固定し、scale(.9)からscale(1)へ0.22秒で拡大します。跳ねるようなcubic-bezierを使っているため、開いた瞬間にわずかな弾みが付きます。右上のプロフィールメニューや、ツールバーのオプションメニューに向いています。
.menu__panel {
transform-origin: top right;
transform: scale(.9);
opacity: 0;
}
.menu.is-open .menu__panel {
transform: scale(1);
opacity: 1;
transition: transform .22s cubic-bezier(.2, 1.4, .4, 1),
opacity .22s cubic-bezier(.2, 1.4, .4, 1);
}
04クリップパスで開く
clip-pathの矩形がinset(0 0 100% 0)からinset(0 0 0% 0)へ0.3秒かけて動き、パネルの中身を上から切り出すように見せます。カード内のコンテキストメニューや、狭いツールバーの「もっと見る」メニューに向いています。
.menu__panel {
clip-path: inset(0 0 100% 0);
pointer-events: none;
}
.menu.is-open .menu__panel {
clip-path: inset(0 0 0% 0);
pointer-events: auto;
transition: clip-path .3s ease-out;
}
05項目が順番に表示
パネルが開くと項目は一斉に現れず、各liのtransition-delayに(i-1)×20msを足すことで、上から20msずつ遅れて一つずつ浮かび上がります。項目数の多い設定メニューや、フィルターオプションのドロップダウンに向いています。
.menu__panel li {
opacity: 0;
transform: translateY(-6px);
}
.menu.is-open .menu__panel li {
opacity: 1;
transform: none;
transition: opacity .22s ease-out, transform .22s ease-out;
}
@for $i from 1 through 4 {
.menu.is-open .menu__panel li:nth-child(#{$i}) {
transition-delay: ($i - 1) * 20ms;
}
}
06メガメニュー背景の塗りつぶし
背景の.menu__fillがtransform-origin: leftを起点にscaleX(0)からscaleX(1)へ0.3秒で横に広がり、続けて.menu__gridの不透明度が上がることで、複数列のカテゴリを一枚のパネルに展開します。ECサイト上部のカテゴリメガメニューや、多段の商品ナビゲーションに向いた発想の型です。
.menu__fill {
transform: scaleX(0);
transform-origin: left;
}
.menu__grid { opacity: 0; }
.menu.is-open .menu__fill {
transform: scaleX(1);
transition: transform .3s ease-out;
}
.menu.is-open .menu__grid {
opacity: 1;
transition: opacity .3s ease-out .12s;
}
07遅延して開く(transition-delay)
トリガーにカーソルが乗っただけでは開かないよう、:hoverと.is-openのtransition-delayだけに150msを足し、素通りしたカーソルでは開かせません。閉じるときはこの遅延付きセレクタが外れるため、間を置かず0.22秒で閉じます。横に並んだ複数のヘッダーメニューや、誤って開いてはいけないナビゲーションに向いた発想の型です。
.menu__panel {
opacity: 0;
transform: translateY(-6px);
transition: opacity .22s ease-out, transform .22s ease-out;
}
.menu:hover .menu__panel,
.menu.is-open .menu__panel {
opacity: 1;
transform: none;
transition-delay: 150ms;
}
08popover属性を使用
popover属性と::backdropを使うと、開閉・バックドロップの表示・Escキーでの閉じる動作をブラウザが代わりに管理し、JSのトグル処理を書かずに済みます。軽いアクションメニューや、JSバンドルを増やしたくないページに向いた発想の型です。
.menu__panel {
transform: scale(.9);
opacity: 0;
}
.menu__panel::backdrop {
background: rgba(0, 0, 0, .18);
}
.menu__panel:popover-open {
transform: scale(1);
opacity: 1;
transition: transform .3s cubic-bezier(.2, 1.4, .4, 1),
opacity .3s cubic-bezier(.2, 1.4, .4, 1);
}
09モバイル全画面シート
狭い画面ではドロップダウンの代わりに、.menu__sheetがtranslateY(100%)から0へ0.3秒かけて画面下からせり上がり、指で押しやすい全幅のシートになります。max-width: 360pxのメディアクエリではenv(safe-area-inset-bottom)も足し、ノッチのある端末でも項目が隠れません。モバイルWebのナビゲーションメニューや、レスポンシブヘッダーの「もっと見る」メニューに向いています。
.menu__sheet {
transform: translateY(100%);
}
.menu__sheet.is-open {
transform: translateY(0);
transition: transform .3s ease-out;
}
@media (max-width: 360px) {
.menu__sheet {
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
}
}
どこで崩れるか — 落とし穴
9つとも高さや座標の数値を直接書き換えず、grid-template-rows・opacity・transform・clip-pathという補間できる値だけを動かしています。1つ目の落とし穴は01を組み立てているときに踏んだもので、内側の.menu__listにoverflow: hiddenを付け忘れると、grid-template-rowsが0frの状態でも中の文字がはみ出して見えてしまいました。高さの単位そのものを補間するinterpolate-sizeというプロパティもありますが、MDNの対応状況によると2026年9月時点ではChrome 129以降にしか実装されておらず、SafariとFirefoxではまだ使えないため、この記事では全ブラウザで動くgrid-template-rowsのトリックを選びました。
2つ目は06のメガメニューを作っていたときのことです。背景の.menu__fillと項目の.menu__gridを同時に不透明度1へ変えると、背景がまだ左端しか塗られていない途中の段階でリンクの文字が先に見えてしまい、文字だけが余白の上に浮いているように見えました。.menu__gridのtransitionに0.3秒の40%にあたる0.12秒の遅延を足し、背景が塗り終わってから項目を出すようにしてから、この崩れは直りました。修正済みの9ファイルを開くには wncdnnc7 という文字列が必要です。下のカードからzipを取ってください。
3つ目は08のpopover属性を試したときのことです。popoverはJSのトグルなしに開閉・バックドロップ・Escキーをブラウザが代わりに処理してくれますが、MDNのPopover API対応表によるとChrome 114以降・Safari 17以降・Firefox 125以降でしか動かず、それより古いブラウザではpopover属性ごと無視されるため、パネルは開閉できないまま常に非表示で残ります。popoverだけに頼らず、対応していないブラウザ向けに別の開閉手段を残しておく必要があると分かりました。
アクセシビリティ
9つともprefers-reduced-motion: reduceで開閉のtransitionとアニメーションが止まりますが、要素そのものは消えません。01のgrid-template-rowsは0frのまま、02のフェード+スライドと03の拡大、04のクリップパス、05の項目、07の遅延メニューはどれも「閉じた状態の見た目」で静止します。06はメガメニューの背景.menu__fillがscaleX(0)のまま、.menu__gridがopacity 0のままで止まり、09の全画面シートはtranslateY(100%)のまま画面外に残ります。08のpopoverだけは逆に「開いた状態」で止まり、transform: scale(1)とopacity: 1を保ったまま表示され続けます。
@media (prefers-reduced-motion: reduce) {
.menu__panel, .menu__panel li,
.menu__fill, .menu__grid,
.menu__sheet { transition: none !important; animation: none !important; }
.menu.is-demo .menu__panel { grid-template-rows: 0fr; }
.menu__panel:popover-open { transform: scale(1); opacity: 1; }
}
ドロップダウン以外のCSSアニメーションはこちらのカテゴリー一覧に、このサイトの成り立ちは紹介ページにまとめています。
FAQ
9つを同じページに複数置いても動きは重なりませんか?
重なりません。それぞれのtransitionやanimationは自分自身のクラス名にしか結び付いていないため、01のgrid-template-rowsと06のscaleXを同じページに並べても、片方の動きがもう片方の要素に影響することはありません。08のpopoverだけはidで対象を指定するため、複数置く場合はpopovertargetとidの組を項目ごとに変える必要があります。
Reactでは何を使って開閉を実装しますか?
zipのreact/フォルダの各コンポーネントは、is-openのようなbooleanをuseStateで持ち、そのstateに応じてクラス名を付け外しするだけで、開閉の速さと軌道はCSSのtransitionとanimationが担います。08だけはネイティブなpopover属性を使うため、booleanではなくref経由でshowPopover()とhidePopover()を呼び分ける形になっています。
スマホでも同じように開閉しますか?
はい、01から07はモバイルのSafariやChromeでも同じ速さで開閉します。09はもともと狭い画面向けに全幅シートへ切り替わる作りです。08のpopoverは前述のMDNの対応表にあるとおりSafari 17以降で動きますが、それより古いiOSでは属性ごと無視され、メニューが開閉できないまま残ります。