GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

【JS最小・CSSだけ】css ハンバーガーメニュー アニメーション9選

css ハンバーガーメニュー アニメーション は、3本線のアイコンをクリックした瞬間に何が起こるかを決める短い動きです。この記事では9種類を選び、CSS の transform と opacity だけで実装しました(JS はクリックを受け取る数行だけ)。

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

グリッドの9つは iframe の中で2秒周期に自動で開閉を繰り返すだけで、クリックしても反応しません。動きを見比べるための「眺める」デモで、実際に押して確かめたいときは下にある項目ごとのデモを開いてください。並び順は効果の種類ではなく、実務でよく使う場面の順です。最初の4つはヘッダーに置く定番のアイコン変形(01〜04)、次の3つは開いた後のパネルやリンクの見せ方(05〜07)、最後の2つは背景側に演出を足すものです(08〜09)。9つとも JavaScript はクリックを受け取る数行だけで、開閉そのものは CSS の transition と transform が担っています。 05のようにメニューが画面を覆う段階を越えて画面ごと入れ替える必要が出たらcss ページ遷移 アニメーション9選の領域になり、メニューではなく項目ひとつがその場で開く場合はCSS アコーディオン アニメーション 9選が近いです。

013本線がXに変形

何が起きるか

上下の線がそれぞれ45度ずつ回転して重なりXになり、中央の線はscaleXとopacityで同時に消えます。

3本線がXに変形
新しいタブで開く

01 · 3本線がXに変形 — 計測 累積面積 9.553% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

300ms45deg가운데 페이드

向いている場面

300msのspringイージング以外に特殊な仕掛けがないので、ヘッダーの基本ボタンとして最初に選ぶならこの形です。

.bar {
  position: absolute; left: 50%; top: 50%;
  width: clamp(20px, 6.4vmin, 26px); height: 3px; border-radius: 2px;
  background: $color;
  transition: transform $duration $easing, opacity $duration $easing;
}
.bar1 { transform: translate(-50%, -50%) translateY(-7px) rotate(0deg); }
.bar2 { transform: translate(-50%, -50%) scaleX(1); opacity: 1; }
.bar3 { transform: translate(-50%, -50%) translateY(7px) rotate(0deg); }

.stage.is-open .bar1 { transform: translate(-50%, -50%) translateY(0) rotate(45deg); }
.stage.is-open .bar2 { transform: translate(-50%, -50%) scaleX(0); opacity: 0; }
.stage.is-open .bar3 { transform: translate(-50%, -50%) translateY(0) rotate(-45deg); }

02回転しながらX

動きの内容

線がXに変形するのと同時に、アイコン全体が180度回ります。

回転しながらX
新しいタブで開く

02 · 回転しながらX — 計測 累積面積 13.016% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

180deg350ms역회전

使う場面

ease-popで終わりに少しだけ弾みを付けているので、強調したいモバイルナビの開閉ボタンに向いています。

$duration: 350ms;         // アイコンが回る時間
$easing: $ease-pop;       // 最後に少し弾む

.burger {
  transform: rotate(0deg);
  transition: transform $duration $easing;
}

.stage.is-open .burger { transform: rotate(180deg); }
.stage.is-open .bar1 { transform: translate(-50%, -50%) translateY(0) rotate(45deg); }
.stage.is-open .bar2 { transform: translate(-50%, -50%) scaleX(0); opacity: 0; }
.stage.is-open .bar3 { transform: translate(-50%, -50%) translateY(0) rotate(-45deg); }

03矢印に変形

どんな動きか

右端を軸に3本の線が閉じていくと、開くボタンではなく戻る矢印のような形になります。

矢印に変形
新しいタブで開く

03 · 矢印に変形 — 計測 累積面積 13.206% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

250ms오른쪽 화살표각도 30deg

使いどころ

中央の線は消えずscaleX(.7)で少し縮むだけなので、戻る操作を兼ねるメニューボタンに使えます。

$duration: 250ms;          // 矢印に狭まる時間
$easing: $ease-spring;

// 右端を軸に回転させ、左端が閉じると "<" の形になる
.bar {
  transform-origin: right center;
}
.bar1 { transform: translate(50%, -50%) translateY(-7px) rotate(0deg); }
.bar2 { transform: translate(50%, -50%) scaleX(1); opacity: 1; }
.bar3 { transform: translate(50%, -50%) translateY(7px) rotate(0deg); }

.stage.is-open .bar1 { transform: translate(50%, -50%) translateY(-3px) rotate(-35deg); }
.stage.is-open .bar2 { transform: translate(50%, -50%) scaleX(.7); opacity: 1; }
.stage.is-open .bar3 { transform: translate(50%, -50%) translateY(3px) rotate(35deg); }

04中央線がスライドアウト

何が起きるか

開くと中央の線だけが横に抜けて消え、そのあとで上下の線がXに重なります。

中央線がスライドアウト
新しいタブで開く

04 · 中央線がスライドアウト — 計測 累積面積 13.229% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

오른쪽으로280ms왼쪽으로

向いている場面

keyframeでは中央線が8%地点、上下線が15%地点と、少しだけタイミングをずらしているのがこの項目の要点です。

$duration: 280ms;         // 中央線が抜ける時間

.stage.is-open .bar1 { transform: translate(-50%, -50%) translateY(0) rotate(45deg); }
.stage.is-open .bar2 { transform: translate(-50%, -50%) translateX(140%); opacity: 0; }
.stage.is-open .bar3 { transform: translate(-50%, -50%) translateY(0) rotate(-45deg); }

// 中央線が先に抜け、そのあと上下線がXに重なる。閉じるときは逆順
@keyframes bar2-loop {
  0%   { transform: translate(-50%, -50%) translateX(0); opacity: 1; }
  8%   { transform: translate(-50%, -50%) translateX(140%); opacity: 0; }
  78%  { transform: translate(-50%, -50%) translateX(140%); opacity: 0; }
  85%  { transform: translate(-50%, -50%) translateX(0); opacity: 1; }
  100% { transform: translate(-50%, -50%) translateX(0); opacity: 1; }
}

05全画面オーバーレイメニュー

動きの内容

ボタンを押すと画面全体を覆う色パネルが下から600msかけてせり上がり、メニューが現れます。

全画面オーバーレイメニュー
新しいタブで開く

05 · 全画面オーバーレイメニュー — 計測 累積面積 99.997% · 動いたフレーム 11/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

400ms위로좌우 슬라이드

使う場面

stageにoverflow: hiddenをかけているので、パネルがせり上がっている間もページの他の部分は動きません。

$duration: 600ms;          // パネルがせり上がる時間

.stage { position: relative; overflow: hidden; }

.panel {
  position: absolute; inset: 0; z-index: 2;
  background: $stage-orange;
  transform: translateY(100%);
  transition: transform $duration $easing;
}

.stage.is-open .panel { transform: translateY(0); }

06Popover APIでJS0行メニュー

どんな動きか

popover属性を付けるだけでメニューが開き、外側をクリックすると自動的に閉じます。

Popover APIでJS0行メニュー
新しいタブで開く

06 · Popover APIでJS0行メニュー — 計測 累積面積 12.977% · 動いたフレーム 9/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

popoveranchor자동 닫힘

使いどころ

実際に開閉を切り替えているのは:popover-openセレクタで座標を上書きする部分だけで、開閉のJavaScriptは1行も要りません。

// popover 属性が付いていない間(デモの初期値)は anchor の下に普通の箱として描く
.menu {
  position: absolute; top: calc(100% + 8px); left: 0; right: auto;
  margin: 0; border: 0;
  opacity: 0; transform: translateY(-8px) scale(.96); transform-origin: top left;
  transition: opacity $duration $easing, transform $duration $easing;
}

// 本物の Popover API で開くと top layer の上、同じ位置に座標だけ再計算する
.menu:popover-open {
  position: fixed; inset: auto;
  opacity: 1; transform: translateY(0) scale(1);
}

07リンクが順番に表示

何が起きるか

メニューが開くと、リンクが30ms間隔で上から順に現れます。

リンクが順番に表示
新しいタブで開く

07 · リンクが順番に表示 — 計測 累積面積 12.543% · 動いたフレーム 10/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

30ms역순위에서

css ドロップダウン メニューでの使いどころ

transition-delayにCSSカスタムプロパティ--iを掛け合わせているだけなので、リンクの数が増えてもJavaScript側の計算は増えません。

$stagger: 30ms;            // リンクの間隔
$duration: 220ms;          // リンク1つが現れる時間

.menu__link {
  opacity: 0; transform: translateY(-6px);
  transition: opacity $duration $ease-spring, transform $duration $ease-spring;
  transition-delay: calc(var(--i) * #{$stagger});
}

.stage.is-open .menu__link { opacity: 1; transform: translateY(0); }

08アイコンから円形に広がる

動きの内容

ボタンの位置から円が広がって画面を覆い、メニューが現れます。

アイコンから円形に広がる
新しいタブで開く

08 · アイコンから円形に広がる — 計測 累積面積 99.997% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

clip-path circle450ms오른쪽 위에서

使う場面

動かしているのはclip-path circle()の半径だけで、widthやheightに触れないためレイアウトの再計算は起きません。

$duration: 450ms;         // 円が画面を覆う時間

// アイコンの位置(右上)を起点に、clip-path の半径だけを動かす
.reveal {
  position: absolute; inset: 0; z-index: 2;
  background: $stage-ink;
  clip-path: circle(0% at calc(100% - 30px) 30px);
  transition: clip-path $duration $easing;
}

.stage.is-open .reveal { clip-path: circle(150% at calc(100% - 30px) 30px); }

09背景ぼかし+暗転

どんな動きか

メニューが開くと背後の.pageがfilter: blur()とbrightness()で同時にぼやけて暗くなり、視線がメニューに集まります。

背景ぼかし+暗転
新しいタブで開く

09 · 背景ぼかし+暗転 — 計測 累積面積 20.247% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

6px어둡게 40%10px

使いどころ

半透明の重ね要素を別に用意せず、.page自体に直接filterをかけているのがこの実装の特徴です。

$blur: 6px;                // 背景がぼける強さ
$dim: .4;                  // 背景が暗くなる強さ(0〜1)

.page {
  transition: filter $dur-base $ease-spring;
}

.stage.is-open .page { filter: blur($blur) brightness(1 - $dim); }

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

z-index の順番

css ドロワー メニューに近い05と08で最初につまずきました。オーバーレイやreveal要素にz-index: 2を、ボタンにはz-index: 3を割り当てないと、メニューが開いた瞬間にボタンがパネルの下に隠れ、二度とクリックで閉じられなくなります。閉じる入り口を潰さないためのz-index管理は、背景を覆う系の項目に共通する落とし穴でした。

blur と backdrop-filter

09は背後のページ全体にfilter: blur()をかけていますが、半透明パネルだけをぼかしたい場面ではbackdrop-filterのほうが向きます。backdrop-filterはChrome 76以降、Safariは18以降で接頭辞なしに対応し、Firefoxは103以降で使えます(MDNのbackdrop-filter)。もう一つ、06のようにJavaScriptを0行に保ちたいときは、popoverの代わりにチェックボックスと:has()で状態を拾う古い手も残っていますが、:has()はChrome 105以降、Safari 15.4以降、Firefox 121以降での対応です(MDNの:has())。対応が新しい分、素直に書けるのはpopoverのほうでした。9つとも直したファイルを開ける鍵は 2cpj9dyd という一続きの文字列で、下のボタンから zip を受け取ってください。

07のリンクが順番に出てくる遅延の考え方は、メニューの外でも使えます。同じ計算を縦に長く並べたものがタイムライン デザイン css 9選に段階ごとにまとめてあります。

アクセシビリティ

動きを減らす設定のとき

9つとも prefers-reduced-motion: reduce では、グリッドとデモが繰り返すループ演出だけを止め、transitionを1msに縮めます。クリックによる本来の開閉自体は今までどおり働くので、動きを止めても機能は失われません。ボタンのaria-labelは開閉のたびに「メニューを開く」「メニューを閉じる」に書き換わり、06のpopoverだけはEscキーでの閉じ方やフォーカスの戻し方をブラウザが標準で面倒を見てくれます。

このほかのCSSアニメーションはCSSアニメーションの分類に、このサイトの成り立ちはこのサイトについてにまとめています。

FAQ

9つを1つのサイトにまとめて使っても大丈夫ですか?

構いません。それぞれtransform-originやz-indexの取り方が独立しているので、ページごとに違う項目を使い分けても衝突しません。ただし同じヘッダーの中で2種類の開閉ボタンを並べると、訪問者がどちらが本物のメニューか迷うので、1ページには1種類に絞るのがおすすめです。

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

zipのreact/フォルダのコンポーネントはuseStateの真偽値ひとつでクラス名を切り替えるだけで、01〜05・07〜09はそれで完結します。06だけはpopovertarget属性の代わりにrefを持たせ、showPopover()とhidePopover()を呼ぶ形にしてあります。

ドロワー メニュー css のみでもスマホで同じように開閉しますか?

はい、popoverもtransformもHTMLとCSSの標準機能なので、iOSのSafariでもAndroidのChromeでも同じように動きます。対応状況の詳細はMDNのPopover APIのページで確認できます。 なお画面が広いときは、同じナビゲーションでもハンバーガーより開いて広がる形が自然になります。その形はCSS ドロップダウン メニュー アニメーション 9選に別途まとめました。

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

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