デモ
デモを読み込めませんでした。
仕様
- 用途
- ナビゲーション
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
ページの上に止めたヘッダーを、スクロールに合わせて低くし、白い地と下の罫線・影を付けて、ロゴも小さくします。ScrollTrigger で、先頭から 150px 送るあいだの量にそのまま結ぶので(scrub)、途中で止めれば途中の形で止まり、戻せば同じだけ広がります。
高さ・地・罫線・影・ロゴの倍率は、どれも CSS の変数 --shrink(0 から 1)ひとつから計算しています。JS が動かすのはこの値だけなので、色や大きさは CSS だけで直せます。高さは 112px から 64px(ナビの上下の余白が 32px から 8px)、ロゴは 0.72 倍で、デザインの決まりのロゴの大きさに収まります。150px で縮めきると、見出しを読み始める前に落ち着きます。
下の罫線は border ではなく内側の影で引き、落ちる影と 1 つにまとめます。border で引くと、線の太さのぶん中身の上下の中央が 0.5px ずれます。ロゴは scale で縮め、原点を左端に置きます。文字の大きさを動かすと並びが計算し直され、ナビの位置までずれます。本文の先頭には高いときのヘッダーの分の余白を取っておくと、縮んでも本文は動きません。
動きを減らす設定では、途中を見せずに、150px を越えたところで縮んだヘッダーへ切り替えます。スクロールに連動させず、一定量を越えたら 0.35 秒ほどで縮める形にしたいときは、同じ変数を ScrollTrigger の出入りで動かします。項目が並びきらない幅ではナビを外し、ロゴとボタンだけにしています。
EC・ブランドサイト・メディアの共通ヘッダーに向いています。先頭では大きなロゴと余白で雰囲気を見せ、読み始めたら画面を本文に返せます。
コード
<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="/shop/" aria-current="page">商品一覧</a></li>
<li><a href="/rooms/">お部屋</a></li>
<li><a href="/materials/">素材</a></li>
<li><a href="/journal/">読みもの</a></li>
<li><a href="/showrooms/">ショールーム</a></li>
</ul>
</nav>
<a class="site-header__cart" href="/cart/">カート(2)</a>
<a class="site-header__cta" href="/visit/">来店を予約</a>
</header>
<!-- 本文の先頭は、高いときのヘッダーの分だけ空けておく(縮んでも本文は動かない) -->
<main class="shop">
<p class="shop__label">Spring collection · 2026</p>
<h1 class="shop__title">深呼吸したくなる、<br>そんな部屋を。</h1>
<p class="shop__lead">ここに説明文が入ります。</p>
<img class="shop__hero" src="/img/living-room.jpg" alt="" width="1600" height="600">
<h2 class="shop__heading">今季の新作</h2>
<ul class="shop__grid">
<li>
<img src="/img/arc-lounge-chair.jpg" alt="" width="800" height="560">
<h3>アーチ型ラウンジチェア <span>¥168,000</span></h3>
<p>オーク・ブークレ</p>
</li>
<li>
<img src="/img/dune-table-lamp.jpg" alt="" width="800" height="560">
<h3>陶器のテーブルランプ <span>¥42,900</span></h3>
<p>陶器・リネンのシェード</p>
</li>
<li>
<img src="/img/pebble-vase-set.jpg" alt="" width="800" height="560">
<h3>丸い花器 <span>¥19,800</span></h3>
<p>炻器・3点セット</p>
</li>
</ul>
</main>
:root {
--header-tall: 112px; /* 先頭にいるとき。ナビ(高さ 48px)の上下に 32px */
--header-short: 64px; /* 縮んだとき。上下に 8px */
--logo-small: 0.72; /* 縮んだときのロゴの倍率(25px → 18px) */
}
/* --shrink(0 = 高い、1 = 縮んだ)ひとつから、高さ・地・罫線・影・ロゴの大きさを決める。JS はこの値だけを動かす */
.site-header {
--shrink: 0;
--surface: 255 255 255; /* 縮んだときの地(rgb) */
--shade: 38 38 38; /* 罫線と影の色(rgb) */
--ink: #262626;
--mute: #6b6b6b;
--on-ink: #ffffff; /* 主ボタンの文字 */
position: fixed;
inset: 0 0 auto;
z-index: 10;
display: flex;
align-items: center;
gap: 32px;
box-sizing: border-box;
height: calc(var(--header-tall) - (var(--header-tall) - var(--header-short)) * var(--shrink));
padding: 0 clamp(16px, 7.5vw, 96px);
background-color: rgb(var(--surface) / var(--shrink));
/* 下の罫線は inset の影で引き、落ちる影と 1 つにまとめる(border で引くと、中身の上下の中央が 0.5px ずれる) */
box-shadow:
inset 0 -1px 0 rgb(var(--shade) / calc(0.12 * var(--shrink))),
0 14px 34px -12px rgb(var(--shade) / calc(0.3 * var(--shrink)));
color: var(--ink);
font-size: 14px;
font-weight: 600;
line-height: 1;
letter-spacing: 0.04em;
white-space: nowrap;
}
.site-header a { color: inherit; text-decoration: none; }
.site-header a:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }
/* ロゴは左端を軸に縮める。scale は並びを動かさないので、ナビの位置は変わらない */
.site-header__logo {
flex: none;
font-size: 25px;
font-weight: 700;
letter-spacing: 0.12em;
transform-origin: 0 50%;
scale: calc(1 - (1 - var(--logo-small)) * var(--shrink));
}
.site-header__nav { flex: 1; }
.site-header__nav ul { display: flex; justify-content: center; gap: 32px; margin: 0; padding: 0; list-style: none; }
.site-header__nav a { color: var(--mute); }
.site-header__nav [aria-current="page"] { color: var(--ink); }
.site-header .site-header__cta {
flex: none;
height: 40px;
padding: 0 24px;
border-radius: 999px;
background-color: var(--ink);
color: var(--on-ink);
line-height: 40px;
}
.shop {
padding: calc(var(--header-tall) + 32px) clamp(16px, 7.5vw, 96px) 96px;
background-color: #f2f1ed;
color: #262626;
font-size: 18px;
line-height: 1.8;
letter-spacing: 0.04em;
}
.shop__label { margin: 0 0 24px; color: #6b6b6b; font-size: 13px; font-weight: 700; line-height: 1; letter-spacing: 0.14em; text-transform: uppercase; }
.shop__title { margin: 0 0 24px; font-size: clamp(32px, 5vw, 64px); font-weight: 800; line-height: 1.05; letter-spacing: -0.02em; }
.shop__lead { margin: 0; word-break: auto-phrase; }
.shop__hero { display: block; width: 100%; height: auto; aspect-ratio: 16 / 5; margin-top: 48px; border-radius: 4px; background-color: #e0d2bf; object-fit: cover; }
.shop__heading { margin: 80px 0 24px; font-size: clamp(32px, 4vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; }
.shop__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 32px; margin: 0; padding: 0; list-style: none; }
.shop__grid img { display: block; width: 100%; height: auto; aspect-ratio: 10 / 7; border-radius: 4px; background-color: #e6ddd0; object-fit: cover; }
.shop__grid h3 { display: flex; justify-content: space-between; gap: 16px; margin: 16px 0 4px; font-size: 20px; line-height: 1.3; letter-spacing: -0.01em; word-break: auto-phrase; }
.shop__grid h3 span { font-size: 16px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.shop__grid p { margin: 0; color: #6b6b6b; font-size: 14px; line-height: 1.7; }
/* 項目が並びきらない幅では、ナビを外して(案件ではメニューのボタンへ)ロゴとボタンだけにする */
@media (max-width: 1180px) {
.site-header__nav { display: none; }
.site-header__logo { margin-right: auto; }
}
@media (max-width: 560px) {
:root { --header-tall: 80px; --header-short: 56px; --logo-small: 0.875; }
.site-header { gap: 16px; padding: 0 16px; }
.site-header__logo { font-size: 16px; }
.site-header__cart { display: none; }
.site-header .site-header__cta { padding: 0 16px; }
}
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const SHRINK = 150; // 先頭からこの量(px)を送るあいだに縮めきる
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// 見た目は CSS が --shrink(0〜1)から決めるので、JS はこの値だけを動かす
document.querySelectorAll(".site-header").forEach((header) => {
if (reduceMotion) {
// 動きを減らす設定では、途中を見せずに、縮めきる量を越えたところで切り替える
ScrollTrigger.create({
start: SHRINK,
end: "max",
onEnter: () => gsap.set(header, { "--shrink": 1 }),
onLeaveBack: () => gsap.set(header, { "--shrink": 0 }),
});
return;
}
gsap.fromTo(
header,
{ "--shrink": 0 },
{
"--shrink": 1,
ease: "none", // 送った量に比例させる。戻せば同じだけ広がる
scrollTrigger: {
start: 0,
end: SHRINK,
scrub: true, // スクロール量にそのまま連動させる(数値の scrub は実時間で追うので遅れる)
},
}
);
});