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

ナビゲーションのホバーでドロップダウンが降り、項目が上から順に落ちてくる

デモ

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

仕様

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

しくみと調整

ナビの「機能」にマウスを乗せると、パネルが少し上から降りて開き、中の項目が上から順に落ちてきて小さく弾みます。矢印は 180° 回り、「機能」の文字は明るい色に変わります。開閉は 1 本のタイムラインにまとめてあり、マウスが出ると同じ動きを 1.6 倍の速さで逆再生するので、項目が下の段から消えてからパネルが上がります。

パネルは 0.35 秒の power3.out で、14px 上から降ろしながら 0.97 倍から戻します。起点をパネルの左上寄り(横 20%・上端)にしておくと、「機能」の下から広がって見えます。項目は 0.08 秒後から 0.06 秒ずつずらし、18px 上から back.out(1.6) で落とします。弾みを強くしすぎると、行が隣の行に重なって見えます。

ボタンとパネルは同じ li に入れ、パネルの上の余白をナビとのあいだの橋にします。こうすると、ボタンからパネルへ下りるあいだにマウスが外へ出ず、途中で閉じません。斜めに移ると一瞬外へ出ることがあるので、出てから 0.12 秒待って閉じます。

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

ずらす間隔を 0.04 秒ほどに詰めると、項目の多いメニューでも待たされません。乗った行の淡い地と矢印は CSS の transition で出し、動きを減らす設定では、開閉を 20 倍の速さにしてほぼ一瞬で切り替えます。ヘッダーの製品メニュー、アカウントメニュー、並べ替えの選択肢などに向いています。

コード

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

  <nav class="drop-nav" aria-label="メイン">
    <ul class="drop-nav__menu">
      <!-- ボタンとパネルを同じ li に入れる。パネルへ下りるあいだも li の中なので、途中で閉じない -->
      <li class="drop-nav__item">
        <button class="drop-nav__trigger" type="button" aria-expanded="false" aria-controls="menu-features">
          機能
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 9l6 6 6-6" /></svg>
        </button>
        <!-- 閉じている間は visibility: hidden なので、タブ移動と読み上げから外れる -->
        <div class="drop-nav__panel" id="menu-features">
          <ul class="drop-nav__list">
            <li class="drop-nav__row">
              <a class="drop-nav__link" href="/analytics/">
                <svg class="drop-nav__icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M2 15V9M8 15V1M14 15V5" /></svg>
                <span class="drop-nav__text"><strong>アクセス解析</strong><span>ここに説明文が入ります。</span></span>
                <span class="drop-nav__arrow" aria-hidden="true">→</span>
              </a>
            </li>
            <li class="drop-nav__row">
              <a class="drop-nav__link" href="/automation/">
                <svg class="drop-nav__icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><circle cx="8" cy="8" r="7" /></svg>
                <span class="drop-nav__text"><strong>業務の自動化</strong><span>ここに説明文が入ります。</span></span>
                <span class="drop-nav__arrow" aria-hidden="true">→</span>
              </a>
            </li>
            <li class="drop-nav__row">
              <a class="drop-nav__link" href="/integrations/">
                <svg class="drop-nav__icon drop-nav__icon--dots" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3h0M13 3h0M3 13h0M13 13h0" /></svg>
                <span class="drop-nav__text"><strong>外部サービス連携</strong><span>ここに説明文が入ります。</span></span>
                <span class="drop-nav__arrow" aria-hidden="true">→</span>
              </a>
            </li>
            <li class="drop-nav__row">
              <a class="drop-nav__link" href="/security/">
                <svg class="drop-nav__icon" viewBox="0 0 16 16" aria-hidden="true" focusable="false"><rect x="3" y="3" width="10" height="10" rx="1.5" transform="rotate(45 8 8)" /></svg>
                <span class="drop-nav__text"><strong>セキュリティ</strong><span>ここに説明文が入ります。</span></span>
                <span class="drop-nav__arrow" aria-hidden="true">→</span>
              </a>
            </li>
          </ul>
        </div>
      </li>
      <li><a href="/cases/">導入事例</a></li>
      <li><a href="/pricing/">料金プラン</a></li>
      <li><a href="/company/">会社概要</a></li>
    </ul>
  </nav>
</header>

CSS
.drop-header {
  --ground: #111111;                  /* ヘッダーの地 */
  --surface: #262626;                 /* パネルの面 */
  --ink: #eeece6;                     /* 文字・開いている項目 */
  --mute: #a8a6a0;                    /* ほかの項目・説明 */
  --line: rgba(238, 236, 230, 0.14);  /* パネルとアイコンの縁 */
  --hover: rgba(238, 236, 230, 0.08); /* 乗った行の地 */
  position: relative;
  z-index: 10; /* 開いたパネルを、下のページより上に出す */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  column-gap: 40px;
  padding: 32px clamp(16px, 7.5vw, 96px);
  background-color: var(--ground);
  color: var(--ink);
}

.drop-header a { color: inherit; text-decoration: none; }
.drop-header__logo { font-size: 18px; font-weight: 700; letter-spacing: 0.12em; white-space: nowrap; }

.drop-nav { color: var(--mute); font-size: 14px; font-weight: 600; letter-spacing: 0.04em; }
.drop-nav__menu, .drop-nav__list { margin: 0; padding: 0; list-style: none; }
.drop-nav__menu { display: flex; gap: 32px; height: 48px; white-space: nowrap; }
.drop-nav__menu > li { display: flex; align-items: center; }
.drop-nav__item { position: relative; } /* パネルの位置の基準 */

/* 当たり判定をナビの高さいっぱいにする(パネルとのあいだに隙間を作らない) */
.drop-nav__trigger {
  display: flex;
  align-items: center;
  gap: 4px;
  height: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  letter-spacing: inherit;
  cursor: pointer;
}

.drop-nav__trigger svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
.drop-nav a:focus-visible,
.drop-nav__trigger:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; border-radius: 4px; }

/* 上の余白 8px は、ナビからパネルへ下りるあいだの橋。左は、アイコンの左端を「機能」の字にそろえる(8px + 16px) */
.drop-nav__panel {
  position: absolute;
  top: 100%;
  left: -24px;
  width: min(368px, calc(100vw - 32px));
  padding-top: 8px;
  transform-origin: 20% 8px;
  visibility: hidden;
  opacity: 0;
}

.drop-nav__list { padding: 8px; border-radius: 4px; background-color: var(--surface); box-shadow: inset 0 0 0 1px var(--line); color: var(--ink); }

.drop-nav__link {
  display: grid;
  grid-template-columns: 40px 1fr auto;
  align-items: center;
  column-gap: 16px;
  padding: 12px 16px;
  border-radius: 4px;
  letter-spacing: 0;
  white-space: normal;
  transition: background-color 0.2s linear;
}

.drop-nav__link:hover,
.drop-nav__link:focus-visible { background-color: var(--hover); }
.drop-nav .drop-nav__link:focus-visible { outline-offset: -2px; }

/* 枠つきのアイコン。線は 2px、角と線端は丸く */
.drop-nav__icon { width: 40px; height: 40px; box-sizing: border-box; padding: 12px; overflow: visible; border-radius: 4px; box-shadow: inset 0 0 0 1px var(--line); fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.drop-nav__icon--dots { stroke-width: 6; } /* 長さ 0 の線を、丸い線端で点にする */
.drop-nav__text strong { display: block; font-size: 16px; font-weight: 700; line-height: 1.4; }
.drop-nav__text span { display: block; margin-top: 2px; color: var(--mute); font-size: 14px; font-weight: 400; line-height: 1.6; letter-spacing: 0.04em; }

/* 乗った行の矢印は、左から少しずれて入る */
.drop-nav__arrow { font-size: 16px; opacity: 0; transform: translateX(-10px); transition: opacity 0.25s, transform 0.25s cubic-bezier(0.33, 1, 0.68, 1); }
.drop-nav__link:hover .drop-nav__arrow,
.drop-nav__link:focus-visible .drop-nav__arrow { opacity: 1; transform: none; }

@media (max-width: 600px) {
  .drop-header { padding: 24px 16px; }
  .drop-nav { font-size: 13px; letter-spacing: 0.02em; }
  .drop-nav__menu { gap: 20px; }
  .drop-nav__panel { left: -8px; } /* 画面の左端で切れないように */
}

@media (prefers-reduced-motion: reduce) {
  .drop-nav__link,
  .drop-nav__arrow { 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; // マウスが出てから閉じ始めるまで(秒)

document.querySelectorAll(".drop-nav__item").forEach((item) => {
  const trigger = item.querySelector(".drop-nav__trigger");
  const panel = document.getElementById(trigger.getAttribute("aria-controls"));
  // 色は CSS が正。GSAP は var(--name) のままでは補間できないので、値にして渡す
  const style = getComputedStyle(item);
  const [mute, ink] = ["--mute", "--ink"].map((name) => style.getPropertyValue(name).trim());

  // 開閉は 1 本のタイムライン。閉じるときは逆再生するので、項目は下の段から消えてからパネルが上がる
  const open = gsap
    .timeline({ paused: true })
    .fromTo(panel, { autoAlpha: 0, y: -14, scale: 0.97 }, { autoAlpha: 1, y: 0, scale: 1, duration: 0.35, ease: "power3.out" }, 0)
    // 項目は上から順に、少し上から落ちてきて小さく弾む
    .fromTo(panel.querySelectorAll(".drop-nav__row"), { autoAlpha: 0, y: -18 }, { autoAlpha: 1, y: 0, duration: 0.4, ease: "back.out(1.6)", stagger: 0.06 }, 0.08)
    .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);

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