デモ
デモを読み込めませんでした。
仕様
- 用途
- ナビゲーション
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
記事を下へ読み進めている間、ヘッダーは上へ滑って画面の外に隠れています。少しでも上へ戻せば、すぐに降りてきます。見ているのはスクロールの向きだけで、前の位置と比べるのではなく、ScrollTrigger が持っている向きの値をそのまま読んでいます。
隠すのは0.4秒の power2.in、表示するのは0.45秒の power3.out。表示するほうを強く減速させたのは、上へ戻した人はもうヘッダーを探し始めているので、できるだけ早く迎えたいからです。隠すほうは、読んでいる人に気づかれない程度の速さで足ります。動かす量はヘッダーの高さ分で、影を付けるならその分も足してください。トゥイーンは向きが変わったときだけ overwrite を付けて作り直します。スクロールのたびに作ると前のトゥイーンと取り合いになり、ヘッダーが小刻みに揺れます。
記事の冒頭では隠しません。先頭でわずかに動かしただけでヘッダーが消えると、ページが落ち着かなく見えるので、冒頭の画像が画面の上へ抜けたところを隠し始める位置にしました。読み込んだ直後は、スクロールしていないのに下向きの通知が一度届くことがあり、それで隠れないよう、隠すのはこの位置より下にいるとき(isActive)に限っています。メニューを開いている間や、ページ内リンクで移動した直後も、表示したまま止めておいてください。
この動きがいちばん効くのは、画面の狭いスマートフォンです。ヘッダーが画面の5分の1ほどを占める画面では、隠したときに本文へ返せる広さが大きくなります。項目が並びきらない幅では、ナビを外してロゴとボタンだけにしています。
コード
<header class="site-header">
<a class="site-header__logo" href="/">Hi! I'm HIGASHIYUKI®</a>
<nav class="site-header__nav" aria-label="カテゴリ">
<ul>
<li><a href="/culture/">カルチャー</a></li>
<li><a href="/technology/" aria-current="page">テクノロジー</a></li>
<li><a href="/science/">サイエンス</a></li>
<li><a href="/design/">デザイン</a></li>
</ul>
</nav>
<a class="site-header__cta" href="/subscribe/">購読する</a>
</header>
<article class="article">
<h1 class="article__title">軽いWebページが、<br>また増えてきた</h1>
<!-- 冒頭の画像。ここが画面の上へ抜けるまでは、ヘッダーを隠さない -->
<img class="article-lead" src="/img/slow-web.jpg" alt="" width="1600" height="900">
<p>ここに説明文が入ります。</p>
</article>
.site-header {
--ink: #eeece6;
--mute: #a8a6a0;
--ground: #111111;
position: fixed;
inset: 0 0 auto;
z-index: 10;
display: flex;
align-items: center;
gap: 32px;
box-sizing: border-box;
height: 112px;
padding: 0 6vw;
/* 背景色はページと同じ色。影は付けず、下の縁だけ罫線で区切る */
background-color: var(--ground);
border-bottom: 1px solid rgba(238, 236, 230, 0.14);
color: var(--mute);
font-size: 14px;
font-weight: 600;
letter-spacing: 0.04em;
white-space: nowrap;
}
/* :where() で詳細度を 0 にして、ロゴとボタンの色の指定を勝たせる */
:where(.site-header) a {
color: inherit;
text-decoration: none;
}
.site-header a:focus-visible {
outline: 2px solid var(--ink);
outline-offset: 4px;
}
.site-header__logo {
flex: none;
margin-right: auto;
font-size: 18px;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--ink);
}
.site-header__nav ul {
display: flex;
gap: 32px;
margin: 0;
padding: 0;
list-style: none;
}
.site-header__nav [aria-current="page"] {
color: var(--ink);
}
.site-header__cta {
flex: none;
height: 40px;
padding: 0 24px;
border-radius: 999px;
background-color: var(--ink);
color: var(--ground);
line-height: 40px;
}
/* 本文の先頭は、固定したヘッダーの分だけ空ける */
.article {
max-width: 640px;
margin: 0 auto;
padding: 160px 6vw 144px;
}
.article__title {
margin: 0 0 24px;
font-size: clamp(28px, 3.4vw, 44px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
}
.article-lead {
display: block;
width: 100%;
height: auto;
margin-bottom: 48px;
border-radius: 4px;
}
/* 項目が並びきらない幅では、ナビを外して(実際のサイトではメニューのボタンへ)ロゴとボタンだけにする */
@media (max-width: 900px) {
.site-header__nav { display: none; }
}
@media (max-width: 560px) {
.site-header { gap: 16px; padding: 0 16px; }
.site-header__logo { font-size: 16px; }
.site-header__cta { padding: 0 16px; }
}
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const header = document.querySelector(".site-header");
const lead = document.querySelector(".article-lead");
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 動きを減らす設定では、ヘッダーを表示したままにする
if (header && !reduceMotion) {
let hidden = false;
const setHidden = (next) => {
// 向きが変わったときだけ作る。スクロールのたびに作ると、前のトゥイーンと取り合って揺れる
if (next === hidden) return;
hidden = next;
gsap.to(
header,
next
? // 下の縁の線まで画面の外へ動かす(影を付けるなら、その分も足す)
{ y: -header.offsetHeight, duration: 0.4, ease: "power2.in", overwrite: true }
: { y: 0, duration: 0.45, ease: "power3.out", overwrite: true }
);
};
ScrollTrigger.create({
// 冒頭の画像が画面の上へ抜けるまでは隠さない(先頭からの量で決めるなら start: 100)
trigger: lead ?? document.body,
start: lead ? "bottom top" : 100,
end: "max",
// direction は下へスクロールすると 1、上へ戻すと -1。作った直後に、まだスクロールしていないのに一度だけ呼ばれるコールバック(progress 1・範囲の外)では隠さない
onUpdate: (self) => setHidden(self.direction === 1 && self.isActive),
onLeaveBack: () => setHidden(false), // 冒頭まで戻ったら表示したまま
});
// メニューを開いているあいだや、ページ内リンクで飛んだ直後は setHidden(false) で表示したままにする
}