本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
アイコン・ナビゲーション

ハンバーガーのアイコンが × に変わって戻る

デモ

操作:クリック・カーソルを重ねる。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

線端の丸い、同じ長さの線を 3 本、丸の中央に重ねて置き、上下の線を 8px ずつ(線の太さ 2px と間 6px)ずらして、ハンバーガーにします。

開くときは、まず 0.2 秒で上下の線を中央へ寄せ、中の線は横に縮めながら消します。続けて 0.2 秒後から、上下の線を +45° と −45° に回します(0.35 秒、back.out(2))。寄せてから回すと、× の交点がぶれません。閉じるときは、同じタイムラインを逆再生します。クリックのたびに再生と逆再生を切り替え、aria-expanded も書き換えます。

開いているあいだは、丸と線の明暗を入れ替えます(丸を文字の色で塗り、線を地の色にする)。× だけより、開いていることがはっきり伝わります。

ホバーでは、丸の外に、ぼかしの無い影で薄い輪を出します。こちらも再生と逆再生で出し入れします。丸の塗りは開閉が使っているので、ホバーでは触りません。

寸法は、3 本線を幅 24px・線 2px・間 6px、ボタンを 48px の丸(1px の線)にしています。このデモは、見やすいように比率はそのままで 4 倍に拡大しています。スマホのナビゲーションや、ドロワーの開閉ボタンに向いています。

コード

HTML
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="site-nav" aria-label="メニューを開く">
  <!-- 線は飾りなので読み上げから外す。状態は aria-expanded、名前は aria-label が持つ -->
  <span class="menu-toggle__bar" aria-hidden="true"></span>
  <span class="menu-toggle__bar" aria-hidden="true"></span>
  <span class="menu-toggle__bar" aria-hidden="true"></span>
</button>

<nav id="site-nav" class="site-nav" aria-label="メインメニュー" hidden>
  <ul>
    <li><a href="/works/">制作実績</a></li>
    <li><a href="/service/">サービス</a></li>
    <li><a href="/contact/">お問い合わせ</a></li>
  </ul>
</nav>
CSS
.menu-toggle {
  --ink: #eeece6; /* 線と枠の色 */
  --ground: #111111; /* 地の色。開いている間の線の色になる */
  --ring: 4px; /* ホバーで出る輪の太さ */
  --ring-color: rgba(238, 236, 230, 0.2); /* 輪の色。--ink と別に持つ(色の形式に関わらず薄くできる) */
  --step: 8px; /* 上下の線を中央からずらす量(線 2px + 間 6px) */
  position: relative;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid var(--ink);
  border-radius: 50%;
  background-color: transparent;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

/* 3 本とも中央に重ねて置き、上下だけ --step ぶんずらす。× にするときは中央へ戻すだけで済む */
.menu-toggle__bar {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 24px;
  height: 2px;
  margin: -1px 0 0 -12px;
  border-radius: 1px; /* 線端を丸くする */
  background-color: var(--ink);
}

.menu-toggle__bar:first-of-type { transform: translateY(calc(var(--step) * -1)); }
.menu-toggle__bar:last-of-type { transform: translateY(var(--step)); }

.menu-toggle:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 4px;
}

/* 開いている間だけ出すメニュー。位置や見た目はサイトに合わせて書き換える */
.site-nav ul {
  margin: 16px 0 0;
  padding: 0;
  list-style: none;
}

.site-nav a {
  display: inline-block;
  padding: 8px 0;
  color: var(--ink, #eeece6);
}
JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const CLEAR = "rgba(0, 0, 0, 0)"; // 閉じている間の丸の中(塗らない)と、輪の始まり

document.querySelectorAll(".menu-toggle").forEach((button) => {
  const [upper, middle, lower] = button.querySelectorAll(".menu-toggle__bar");
  const nav = document.getElementById(button.getAttribute("aria-controls"));
  const styles = getComputedStyle(button);
  // 寸法と色は CSS が正。GSAP は var(--name) のままでは補間できないので、値にして渡す
  const step = parseFloat(styles.getPropertyValue("--step"));
  const ring = parseFloat(styles.getPropertyValue("--ring"));
  const ringColor = styles.getPropertyValue("--ring-color").trim();
  const ink = styles.getPropertyValue("--ink").trim();
  const ground = styles.getPropertyValue("--ground").trim();

  const open = gsap
    .timeline({ paused: true })
    // 1. 上下の線を中央へ寄せ、中の線を消す
    .fromTo(upper, { y: -step }, { y: 0, duration: 0.2, ease: "power2.in" }, 0)
    .fromTo(lower, { y: step }, { y: 0, duration: 0.2, ease: "power2.in" }, 0)
    .fromTo(middle, { scaleX: 1, opacity: 1 }, { scaleX: 0, opacity: 0, duration: 0.15, ease: "power2.in" }, 0)
    // 2. 重なった 2 本を逆向きに回す。寄せてから回すと × の交点がぶれない
    .fromTo(upper, { rotation: 0 }, { rotation: 45, duration: 0.35, ease: "back.out(2)" }, 0.2)
    .fromTo(lower, { rotation: 0 }, { rotation: -45, duration: 0.35, ease: "back.out(2)" }, 0.2)
    // 開いている間は明暗を反転する。× だけより「開いている」ことがはっきり伝わる
    .fromTo(button, { backgroundColor: CLEAR }, { backgroundColor: ink, duration: 0.3, ease: "power1.inOut" }, 0.1)
    .fromTo([upper, middle, lower], { backgroundColor: ink }, { backgroundColor: ground, duration: 0.3, ease: "power1.inOut" }, 0.1);

  // ホバーの輪は、ぼかしなしの box-shadow で丸の外に出す。丸の地は開閉が使うので触らない
  const hover = gsap
    .timeline({ paused: true })
    .fromTo(
      button,
      { boxShadow: `0 0 0 0 ${CLEAR}` },
      { boxShadow: `0 0 0 ${ring}px ${ringColor}`, duration: 0.25, ease: "power2.out" }
    );

  // 動きを減らす設定では、ほぼ一瞬で切り替える
  if (reduceMotion) {
    open.timeScale(20);
    hover.timeScale(20);
  }

  let isOpen = false;
  button.addEventListener("click", () => {
    isOpen = !isOpen;
    isOpen ? open.play() : open.reverse(); // 閉じるのは同じタイムラインの逆再生
    button.setAttribute("aria-expanded", String(isOpen));
    button.setAttribute("aria-label", isOpen ? "メニューを閉じる" : "メニューを開く");
    if (nav) nav.hidden = !isOpen;
  });

  const show = () => hover.play();
  const hide = () => hover.reverse();
  button.addEventListener("pointerenter", show);
  button.addEventListener("pointerleave", hide);
  button.addEventListener("focus", show);
  button.addEventListener("blur", hide);
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。