本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100

アイコン・ナビゲーション

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

デモ

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

仕様

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

しくみと調整

ハンバーガーの3本線は、押すと真ん中の線が横に縮んで消え、中央へ寄った上下の線が × に交わります。同時にボタンの丸が塗りつぶされ、丸と線の明暗が入れ替わります。もう一度押せば、同じ道筋をたどって元に戻ります。

寄せる動きと回す動きは、順番に分けました。まず0.2秒で上下の線を中央に集め、そのあとで ±45°へ回します(0.35秒、back.out(2))。寄せきってから回すので、× の交わる点がぶれません。閉じるときは同じタイムラインを逆再生するだけで、クリックのたびに再生と逆再生を切り替え、aria-expanded も書き換えています。

寸法は、幅24pxの線を直径48pxの丸に収めた形で、丸は1pxの線で縁取っています。上下の線は、線の太さと線のあいだの間隔を足した分だけずらしてあります。このデモでは、その比率を保ったまま拡大して見せています。

明暗を入れ替えるのは、× の形だけより、メニューが開いていることがはっきり伝わるからです。丸の塗りをこの切り替えに使っているので、ホバーでは塗りに触れず、ぼかしのない box-shadow で丸の外側に薄い輪を表示することにしました。この輪も、再生と逆再生で出入りさせています。

コード

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,
.site-nav {
  --ink: #eeece6; /* 線・枠線・文字の色 */
  --ground: #111111; /* 背景色。開いている間の線の色にもなる */
}

.menu-toggle {
  --ring: 4px; /* ホバーで表示する輪の太さ */
  --ring-color: #3d3d3c; /* 輪の色。背景色に線の色を 2 割まぜた、透けない色(透ける色だと、明るいページでは輪が見えない) */
  --step: 8px; /* 上下の線を中央からずらす量(線 2px + 間 6px) */
  position: relative;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 1px solid var(--ink);
  border-radius: 50%;
  background-color: var(--ground);
  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 {
  width: fit-content;
  margin-top: 16px;
  padding: 8px 24px;
  border-radius: 12px;
  background: var(--ground);
}

.site-nav ul {
  margin: 0;
  padding: 0;
  list-style: none;
}

.site-nav a {
  display: inline-block;
  padding: 8px 0;
  color: var(--ink);
  white-space: nowrap;
}

.site-nav a:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

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: ground }, { 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 サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。