デモ
デモを読み込めませんでした。
仕様
- 用途
- ナビゲーション
- 動作
- 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・メディアサイトのように、製品や分類の多いサイトのヘッダーに向いています。
コード
<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>
.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; }
}
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));
});