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

ナビゲーションのホバーでメガメニューが幅いっぱいに開き、列と特集カードが順に出る

デモ

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

仕様

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

しくみと調整

ヘッダーの「製品」にマウスを乗せると、ヘッダーのすぐ下から幅いっぱいのパネルが下へ開き、リンクの列が左の列から順に、列の中は上から順に浮かび上がります。右の特集カードは、画像が下からめくれるように出て、寄っていた画像が引いて収まります。後ろのページには半透明の幕をかけ、「製品」の下には墨色の線が中央から伸びます。

パネルは clip-path の inset() で、下の縁を上端から下ろして開きます。0.6 秒の power4.out で、止める位置をパネルの下端より 120px 先にしてあるので、縁は勢いを残したまま下端を越えて開ききります。下端ちょうどで止めると、着くのが遅く見えます。列の項目は 0.12 秒後から 18px 下から上げ、遅れを「列の番号 × 0.07 秒 + 行の番号 × 0.045 秒」にしています。

ボタンとパネルは同じ li に入れ、位置の基準だけをヘッダーにします。こうすると、パネルが幅いっぱいでも、ボタンからパネルへ下りるあいだにマウスが外へ出ません。ボタンの当たり判定はヘッダーの高さいっぱいにし、斜めに移って一瞬外へ出たときのために、出てから 0.12 秒待って閉じます。閉じるときは同じタイムラインを 1.6 倍の速さで逆再生するので、中身が消えてからパネルが上がります。

ホバーの無い指の端末とキーボードでは、ボタンを押して開け閉めします。開いているかは aria-expanded で伝え、開いたら Tab でパネルの中のリンクへ進めます。Esc で閉じてボタンに焦点を戻し、Tab でパネルの外へ出たときや、画面のほかの所を押したときも閉じます。このデモはマウスを乗せて試せます(キーボードと指の操作は、コードのほうで受けます)。

列を増やすときは、列ごとの遅れを小さくすると、出そろうまでが長くなりすぎません。動きを減らす設定では、開閉を 20 倍の速さにしてほぼ一瞬で切り替えます。特集カードは、新着の記事やキャンペーンの告知に差し替えて使えます。SaaS・EC・メディアサイトのように、製品や分類の多いサイトのヘッダーに向いています。

コード

HTML
<header class="mega-header">
  <a class="mega-header__logo" href="/">Hi! I'm HIGASHIYUKI®</a>

  <nav class="mega-nav" aria-label="メイン">
    <ul class="mega-nav__menu">
      <!-- ボタンとパネルを同じ li に入れる。パネルへ下りるあいだも li の中なので、途中で閉じない -->
      <li class="mega-nav__item">
        <button class="mega-nav__trigger" type="button" aria-expanded="false" aria-controls="mega-products">
          製品
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 9l6 6 6-6" /></svg>
        </button>
        <!-- 幅いっぱいのパネル。位置の基準はヘッダー(li には position を付けない)。閉じている間は visibility: hidden -->
        <div class="mega-panel" id="mega-products">
          <div class="mega-panel__grid">
            <div class="mega-panel__col">
              <p class="mega-panel__label">基本機能</p>
              <ul>
                <li><a class="mega-panel__link" href="/analytics/"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 20v-8M12 20V5M19 20v-9" /></svg><span><strong>データ分析</strong>ここに説明文が入ります。</span></a></li>
                <li><a class="mega-panel__link" href="/automation/"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M13 3 5 13.5h6L10 21l8-10.5h-6L13 3z" /></svg><span><strong>ワークフローの自動化</strong>ここに説明文が入ります。</span></a></li>
                <li><a class="mega-panel__link" href="/security/"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 3l7 3v5.5c0 4.2-2.9 7.6-7 9.5-4.1-1.9-7-5.3-7-9.5V6l7-3z" /></svg><span><strong>セキュリティ</strong>ここに説明文が入ります。</span></a></li>
              </ul>
            </div>
            <div class="mega-panel__col">
              <p class="mega-panel__label">開発・連携</p>
              <ul>
                <li><a class="mega-panel__link" href="/integrations/"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 3v5M15 3v5M6.5 8h11v3.5a5.5 5.5 0 0 1-11 0V8zM12 17v4" /></svg><span><strong>外部サービス連携</strong>ここに説明文が入ります。</span></a></li>
                <li><a class="mega-panel__link" href="/developers/"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 7l-5 5 5 5M15 7l5 5-5 5" /></svg><span><strong>開発者向けAPI</strong>ここに説明文が入ります。</span></a></li>
                <li><a class="mega-panel__link" href="/templates/"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="4" y="4" width="7" height="7" rx="1.6" /><rect x="13" y="4" width="7" height="7" rx="1.6" /><rect x="4" y="13" width="7" height="7" rx="1.6" /><rect x="13" y="13" width="7" height="7" rx="1.6" /></svg><span><strong>テンプレート</strong>ここに説明文が入ります。</span></a></li>
              </ul>
            </div>
            <a class="mega-card" href="/blog/board-2/">
              <span class="mega-card__media"><img src="/img/board-2.jpg" alt="" width="760" height="320"></span>
              <span class="mega-card__tag">FEATURED</span>
              <strong class="mega-card__title">ボード2.0で、計画をその場で共有</strong>
              <span class="mega-card__more">発表の記事を読む →</span>
            </a>
          </div>
          <div class="mega-panel__strip">
            <a href="/products/">すべての製品を見る →</a>
            <span><a class="mega-panel__sub" href="/contact/">営業に相談する</a><a href="/demo/">デモを予約する →</a></span>
          </div>
        </div>
      </li>
      <li><a href="/case-studies/">導入事例</a></li>
      <li><a href="/resources/">お役立ち資料</a></li>
      <li><a href="/pricing/">料金</a></li>
    </ul>
  </nav>
</header>

<!-- 後ろのページを少し暗くする幕 -->
<div class="mega-scrim" aria-hidden="true"></div>

CSS
.mega-header {
  --ground: #f2f1ed;                /* ヘッダーと下の帯の地・乗った項目の地 */
  --surface: #ffffff;               /* パネルの面 */
  --ink: #262626;                   /* 文字・開いている項目 */
  --mute: #6b6b6b;                  /* ほかの項目・説明 */
  --line: rgba(38, 38, 38, 0.12);   /* 区切り線・アイコンの枠 */
  --side: clamp(16px, 7.5vw, 96px); /* 左右の余白 */
  position: relative; /* パネルの位置の基準 */
  z-index: 10; /* 幕より上 */
  display: flex;
  gap: 48px;
  height: 112px;
  padding: 0 var(--side);
  background-color: var(--ground);
  box-shadow: 0 1px 0 var(--line);
  color: var(--ink);
}

:where(.mega-header) a { color: inherit; text-decoration: none; }
:where(.mega-header) :is(a, button):focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 4px; }
.mega-header__logo { align-self: center; font-size: 18px; font-weight: 700; letter-spacing: 0.12em; white-space: nowrap; }

.mega-nav { color: var(--mute); font-size: 14px; font-weight: 600; letter-spacing: 0.04em; }
.mega-nav ul { margin: 0; padding: 0; list-style: none; }
.mega-nav__menu { display: flex; gap: 32px; height: 100%; white-space: nowrap; }
.mega-nav__menu > li { display: flex; align-items: center; }

/* 当たり判定をヘッダーの高さいっぱいにする(パネルとのあいだに隙間を作らない) */
.mega-nav__trigger { position: relative; display: flex; align-items: center; gap: 4px; height: 100%; padding: 0; border: 0; background: none; color: inherit; font: inherit; letter-spacing: inherit; cursor: pointer; }
.mega-nav__trigger svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
/* 開いている間、区切り線の上に出る墨色の線。--bar(0〜1)で中央から左右へ伸ばす */
.mega-nav__trigger::after { content: ""; position: absolute; z-index: 1; left: 0; right: 0; bottom: -1px; height: 2px; background-color: var(--ink); clip-path: inset(0 calc(50% - var(--bar, 0) * 50%)); }

/* 下の縁を、上端(閉じた状態)から、下端より 120px 先まで下ろす。下端ちょうどで止めると、着くのが遅く見える */
.mega-panel {
  --reveal: 0;
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  visibility: hidden;
  clip-path: inset(0 0 calc(100% - var(--reveal) * (100% + 120px)) 0);
  background-color: var(--surface);
  box-shadow: inset 0 1px 0 var(--line);
  color: var(--ink);
  font-weight: 400;
  letter-spacing: 0;
  white-space: normal;
}

.mega-panel__grid { display: grid; grid-template-columns: 1fr 1fr 1.12fr; gap: 24px 32px; padding: 32px var(--side); }
.mega-panel__label { margin: 0 0 8px; color: var(--mute); font-size: 12px; font-weight: 700; line-height: 1; letter-spacing: 0.06em; }

.mega-panel__link { display: grid; grid-template-columns: 40px 1fr; align-items: center; column-gap: 16px; margin: 0 -12px; padding: 12px; border-radius: 4px; transition: background-color 0.2s linear; }
.mega-panel__link:hover,
.mega-panel__link:focus-visible { background-color: var(--ground); }
:where(.mega-header) .mega-panel__link:focus-visible { outline-offset: -2px; }
/* 枠つきのアイコン。線は 2px(viewBox 24 を 20px で描くので 2.4) */
.mega-panel__link svg { width: 40px; height: 40px; box-sizing: border-box; padding: 10px; border-radius: 4px; box-shadow: inset 0 0 0 1px var(--line); fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.mega-panel__link span { color: var(--mute); font-size: 14px; line-height: 1.6; letter-spacing: 0.04em; word-break: auto-phrase; }
.mega-panel__link strong { display: block; margin-bottom: 2px; color: var(--ink); font-size: 16px; line-height: 1.4; letter-spacing: 0; }

.mega-card { display: flex; flex-direction: column; align-items: flex-start; }
/* 画像が届くまでの地も、画像に近い色にしておく */
.mega-card__media { align-self: stretch; height: 160px; overflow: hidden; border-radius: 4px; background: linear-gradient(135deg, #857ba5, #ba7069 55%, #cca57c); }
.mega-card__media img { display: block; width: 100%; height: 100%; object-fit: cover; }
.mega-card__tag { margin-top: 16px; padding: 0 8px 0 10px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--line); color: var(--mute); font-size: 12px; font-weight: 700; line-height: 24px; letter-spacing: 0.14em; }
.mega-card__title { margin-top: 8px; font-size: 20px; line-height: 1.3; letter-spacing: -0.01em; word-break: auto-phrase; }
.mega-card__more { margin-top: 8px; font-size: 14px; font-weight: 600; line-height: 1.5; letter-spacing: 0.04em; }

.mega-panel__strip { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px 32px; padding: 16px var(--side); background-color: var(--ground); box-shadow: inset 0 1px 0 var(--line); font-size: 14px; font-weight: 600; line-height: 1.5; letter-spacing: 0.04em; }
.mega-panel__strip span { display: flex; gap: 32px; }
.mega-panel__sub { color: var(--mute); }

/* 後ろのページを少し暗くする幕(文字の色の半透明)。押しても下のページを止めない */
.mega-scrim { position: fixed; inset: 0; z-index: 9; background-color: rgba(38, 38, 38, 0.42); opacity: 0; visibility: hidden; pointer-events: none; }

@media (max-width: 860px) {
  .mega-header { flex-wrap: wrap; height: auto; padding-top: 24px; row-gap: 8px; }
  .mega-header__logo { flex-basis: 100%; }
  .mega-nav__menu { height: 56px; gap: 24px; }
  .mega-panel__grid { grid-template-columns: 1fr 1fr; }
  .mega-card { grid-column: 1 / -1; }
}

@media (max-width: 600px) {
  .mega-header { --side: 16px; }
  .mega-nav { font-size: 13px; letter-spacing: 0.02em; }
  .mega-nav__menu { gap: 16px; }
  .mega-panel__grid { grid-template-columns: 1fr; }
  .mega-card__media { height: 120px; }
}

@media (prefers-reduced-motion: reduce) {
  .mega-panel__link { transition: none; }
}

JavaScript
import { gsap } from "gsap";

const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const SPEED = reduceMotion ? 20 : 1; // 動きを減らす設定では、ほぼ一瞬で開け閉めする
const CLOSE_SPEED = 1.6; // 閉じるほうは速く
const CLOSE_DELAY = 0.12; // マウスが出てから閉じ始めるまで(秒)
const COL_DELAY = 0.07; // 列ごとの遅れ(秒)
const ROW_DELAY = 0.045; // 列の中の 1 行ごとの遅れ(秒)

document.querySelectorAll(".mega-nav__item").forEach((item) => {
  const trigger = item.querySelector(".mega-nav__trigger");
  const panel = document.getElementById(trigger.getAttribute("aria-controls"));
  const scrim = document.querySelector(".mega-scrim");
  const media = panel.querySelector(".mega-card__media");
  // 色は CSS が正。GSAP は var(--name) のままでは補間できないので、値にして渡す
  const style = getComputedStyle(item);
  const [mute, ink] = ["--mute", "--ink"].map((name) => style.getPropertyValue(name).trim());
  // 列の見出しと項目。左の列から、列の中は上から順に出す
  const cells = [];
  const delays = [];
  panel.querySelectorAll(".mega-panel__col").forEach((col, c) => {
    col.querySelectorAll(".mega-panel__label, li").forEach((cell, r) => {
      cells.push(cell);
      delays.push(c * COL_DELAY + r * ROW_DELAY);
    });
  });

  // 開閉は 1 本のタイムライン。閉じるときは逆再生するので、中身が消えてからパネルが上がる
  const open = gsap
    .timeline({ paused: true, onReverseComplete: () => gsap.set(panel, { visibility: "hidden" }) })
    .fromTo(panel, { "--reveal": 0 }, { "--reveal": 1, duration: 0.6, ease: "power4.out" }, 0)
    .fromTo(scrim, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.45, ease: "power1.out" }, 0)
    .fromTo(trigger.querySelector("svg"), { rotation: 0 }, { rotation: 180, duration: 0.3, ease: "power2.out" }, 0)
    .fromTo(trigger, { color: mute }, { color: ink, duration: 0.2, ease: "none" }, 0)
    .fromTo(trigger, { "--bar": 0 }, { "--bar": 1, duration: 0.35, ease: "power3.out" }, 0.05)
    .fromTo(cells, { autoAlpha: 0, y: 18 }, { autoAlpha: 1, y: 0, duration: 0.5, ease: "power3.out", stagger: (i) => delays[i] }, 0.12)
    // 特集の画像は下からめくり、寄っていた画像を引いて収める
    .fromTo(media, { clipPath: "inset(100% 0% 0% 0% round 4px)" }, { clipPath: "inset(0% 0% 0% 0% round 4px)", duration: 0.8, ease: "power3.out" }, 0.26)
    .fromTo(media.querySelector("img"), { scale: 1.25 }, { scale: 1, duration: 0.8, ease: "power3.out" }, 0.26)
    .fromTo(panel.querySelectorAll(".mega-card > :not(.mega-card__media)"), { autoAlpha: 0, y: 12 }, { autoAlpha: 1, y: 0, duration: 0.45, ease: "power3.out", stagger: 0.06 }, 0.42)
    .fromTo(panel.querySelector(".mega-panel__strip"), { autoAlpha: 0, y: 7 }, { autoAlpha: 1, y: 0, duration: 0.4, ease: "power2.out" }, 0.4);

  let isOpen = false;
  let hovered = false; // マウスを乗せて開いたか(乗せて開いた直後の click では閉じない)
  let closing = null;
  const setOpen = (next) => {
    closing?.kill();
    if (next === isOpen) return;
    isOpen = next;
    trigger.setAttribute("aria-expanded", String(next));
    if (next) {
      gsap.set(panel, { visibility: "visible" });
      open.timeScale(SPEED).play();
    } else {
      open.timeScale(SPEED * CLOSE_SPEED).reverse();
    }
  };

  // マウスは乗せただけで開き、出たら少し待って閉じる(斜めにパネルへ移るあいだに閉じない)
  item.addEventListener("pointerenter", (event) => {
    if (event.pointerType !== "mouse") return;
    hovered = !isOpen;
    setOpen(true);
  });
  item.addEventListener("pointerleave", (event) => {
    hovered = false;
    if (event.pointerType === "mouse") closing = gsap.delayedCall(CLOSE_DELAY, () => setOpen(false));
  });
  // 指とキーボードは押して開け閉めする
  trigger.addEventListener("click", () => {
    setOpen(hovered || !isOpen);
    hovered = false;
  });
  // Esc で閉じて、焦点を項目のボタンへ戻す
  item.addEventListener("keydown", (event) => {
    if (event.key !== "Escape" || !isOpen) return;
    trigger.focus();
    setOpen(false);
  });
  // Tab で li の外へ出たら閉じる。指で外を押したときも閉じる
  item.addEventListener("focusout", (event) => !item.contains(event.relatedTarget) && setOpen(false));
  document.addEventListener("pointerdown", (event) => !item.contains(event.target) && setOpen(false));
});

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

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