デモ
デモを読み込めませんでした。
仕様
- 用途
- ボタン
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
ヒーローのいちばん下、中央の「SCROLL」という小さな英字の下に縦の細い線があり、その中を明るく短い光が上から下へ流れていきます。押せば次のセクションまでなめらかにスクロールして、ボタンは消えます。先頭の近くまで戻ってくると、ボタンがまた現れて光が流れます。
光の正体は、下の端だけが白いグラデーションの短い線です。線の要素に overflow: hidden を付けた中を、上の外から下の外まで1.6秒の power1.inOut で通しているので、線に差し込んで抜けていくように見えます。動き続けるものは読む人の気を散らすので、流すのは表示されたときの2回だけにして、5秒を越えて動かないようにしました。止まったあとでも、カーソルを重ねたりキーボードでフォーカスしたりすると、もう一度流れます。
スクロールは ScrollToPlugin で次のセクションの上端まで運びますが、途中でホイールや指で動かされたら、autoKill でその場で止めます。読む人が動かしているのに、自動のスクロールが引き戻すようなことはしたくありません。ボタンが消えるのは、ヒーローの高さの10%を越えてスクロールしたとき。その境目を越えたか戻ったかは ScrollTrigger で見ています。
ボタンの中身は次のセクションへのページ内リンクなので、スクリプトが動かなくても、ふつうのリンクとして移動できます。線と英字は飾りとしてスクリーンリーダーから外し、名前は aria-label で「次のセクションへ」としました。押すとボタン自体が消えてしまうので、スクロールし終えたところでフォーカスを次のセクションへ移しています。
コード
<section class="hero" aria-labelledby="hero-title">
<div class="hero__bar">
<a class="hero__logo" href="/">Hi! I'm HIGASHIYUKI®</a>
<nav class="hero__nav" aria-label="メイン">
<a href="/works/">設計事例</a>
<a href="#about">設計室について</a>
<a href="/contact/">お問い合わせ</a>
</nav>
</div>
<div class="hero__copy">
<p class="hero__eyebrow">Architecture studio</p>
<h1 class="hero__title" id="hero-title">住む人と、<wbr>ゆっくり家を<wbr>つくる</h1>
<p class="hero__text">ここに説明文が入ります。</p>
</div>
<!-- 次のセクションへのページ内リンク。JS が動かなくても #about へ移る。線と英字は飾りなので、名前は aria-label で付ける -->
<a class="scroll-hint" href="#about" aria-label="次のセクションへ">
<span class="scroll-hint__label" aria-hidden="true">Scroll</span>
<span class="scroll-hint__line" aria-hidden="true"><span class="scroll-hint__light"></span></span>
</a>
</section>
<!-- tabindex="-1" は、スクロールし終えたあとにフォーカスを移すため -->
<section class="about" id="about" tabindex="-1" aria-labelledby="about-title">
<div class="about__text">
<p class="about__eyebrow">About</p>
<h2 class="about__title" id="about-title">設計室について</h2>
<p>ここに本文が入ります。ここに本文が入ります。ここに本文が入ります。</p>
</div>
<img class="about__photo" src="/img/about.jpg" alt="" width="896" height="896">
</section>
.hero {
--bg: #111111;
--ink: #eeece6;
--mute-small: #a8a6a0; /* 16px 未満の文字 */
--line: rgb(238 236 230 / 0.14);
--track: rgb(255 255 255 / 0.18); /* 光が流れる線 */
--track-hover: rgb(255 255 255 / 0.34);
--light: 24px; /* 光の長さ */
--length: 64px; /* 線の長さ */
position: relative;
box-sizing: border-box;
min-height: 100svh;
display: grid;
place-items: center;
padding: 128px clamp(16px, 5vw, 96px) 160px;
background: var(--bg);
color: var(--ink);
text-align: center;
}
.hero__bar { position: absolute; top: 32px; left: clamp(16px, 5vw, 96px); right: clamp(16px, 5vw, 96px); height: 48px; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 8px 24px; }
.hero__logo { flex: none; color: inherit; font-size: 18px; font-weight: 700; letter-spacing: 0.12em; text-decoration: none; white-space: nowrap; }
.hero__nav { display: flex; gap: 32px; }
.hero__nav a { color: inherit; font-size: 14px; font-weight: 600; letter-spacing: 0.04em; text-decoration: none; white-space: nowrap; }
.hero__eyebrow { margin: 0; color: var(--mute-small); font-size: 13px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
.hero__title { margin: 24px 0 0; font-size: clamp(36px, 6vw, 64px); font-weight: 800; line-height: 1.2; letter-spacing: -0.02em; word-break: keep-all; }
.hero__text { margin: 24px 0 0; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; }
/* スクロールのボタン。「SCROLL」の下に縦の線を引き、線の中を光が上から下へ流れる */
.scroll-hint {
position: absolute;
left: 50%;
bottom: 32px;
translate: -50% 0;
display: grid;
justify-items: center;
gap: 12px;
padding: 8px 24px;
color: var(--mute-small);
text-decoration: none;
}
.scroll-hint__label { margin-right: -0.2em; font-size: 11px; font-weight: 700; line-height: 1; letter-spacing: 0.2em; text-transform: uppercase; }
.scroll-hint__line { position: relative; width: 2px; height: var(--length); overflow: hidden; border-radius: 1px; background: var(--track); }
/* 光は線の上の外で待ち、JS が線の下の外まで送る */
.scroll-hint__light { position: absolute; left: 0; top: calc(var(--light) * -1); width: 100%; height: var(--light); background: linear-gradient(180deg, rgb(255 255 255 / 0), #ffffff); }
.scroll-hint:hover { color: #ffffff; }
.scroll-hint:hover .scroll-hint__line { background: var(--track-hover); }
.scroll-hint:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 4px; }
/* 動きを減らす設定では、光を線の中ほどに止めておく */
@media (prefers-reduced-motion: reduce) {
.scroll-hint__light { top: calc(50% - var(--light) / 2); }
}
.about {
--bg: #f2f1ed;
--ink: #262626;
--mute-small: #6b6b6b;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 448px);
align-items: center;
gap: 48px 96px;
padding: 128px clamp(16px, 5vw, 96px);
background: var(--bg);
color: var(--ink);
outline: none;
}
.about__eyebrow { margin: 0; color: var(--mute-small); font-size: 13px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; }
.about__title { margin: 16px 0 0; font-size: clamp(32px, 5vw, 44px); font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; }
.about__text p:last-child { margin: 32px 0 0; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; }
.about__photo { display: block; width: 100%; height: auto; aspect-ratio: 1; border-radius: 4px; background: #bd894c; object-fit: cover; }
@media (max-width: 760px) {
.hero__nav { gap: 16px; }
.about { grid-template-columns: minmax(0, 1fr); }
}
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { ScrollToPlugin } from "gsap/ScrollToPlugin";
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
const HIDE_AFTER = 0.1; // ヒーローの高さのこの割合を越えてスクロールしたら、ボタンを隠す
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".scroll-hint").forEach((hint) => {
const hero = hint.closest("section");
const next = document.querySelector(hint.hash);
const line = hint.querySelector(".scroll-hint__line");
const light = hint.querySelector(".scroll-hint__light");
// 光を線の上の外から下の外まで送る。現れたときに 2 回だけ流して止める(5 秒を越えて動き続けない)
const flow = gsap.timeline({ paused: true, repeat: 1, repeatDelay: 0.8 })
.fromTo(light, { y: 0 }, { y: () => line.offsetHeight + light.offsetHeight, duration: 1.6, ease: "power1.inOut" });
const play = () => reduceMotion || flow.isActive() || flow.restart();
// ヒーローの下のほうまでスクロールしたら隠し、先頭近くへ戻ったらまた表示して光を流す
const show = (on) => gsap.to(hint, {
autoAlpha: on ? 1 : 0,
duration: reduceMotion ? 0 : on ? 0.4 : 0.3,
ease: "power1.out",
overwrite: true,
onStart: on ? play : undefined,
});
ScrollTrigger.create({
start: () => hero.offsetTop + hero.offsetHeight * HIDE_AFTER,
end: "max",
onEnter: () => show(false),
onLeaveBack: () => show(true),
});
// 重ねたとき・フォーカスしたときも、止まっていればもう一度流す
hint.addEventListener("pointerenter", play);
hint.addEventListener("focus", play);
hint.addEventListener("click", (event) => {
event.preventDefault(); // ページ内リンクの一瞬で飛ぶ動きの代わりに、なめらかに送る
gsap.to(window, {
scrollTo: { y: next, autoKill: true }, // 途中でホイールや指で動かしたら、そこで止める
duration: reduceMotion ? 0 : 1.2,
ease: "power2.inOut",
// ボタンは隠れるので、フォーカスを次のセクションへ移す(キーボードで押した人が、そこから読み進められる)
onComplete: () => next.focus({ preventScroll: true }),
});
});
play();
});