本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100

ボタン

縦線の中を光が流れるスクロールボタン。押すと次のセクションへ

デモ

操作:クリック・カーソルを重ねる・スクロール。 「さわってみる」を押すと自動再生が止まり、マウスで動かせます。 触っている間は、ホイールでデモの中がスクロールします。

仕様

用途
ボタン
動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

ヒーローのいちばん下、中央の「SCROLL」という小さな英字の下に縦の細い線があり、その中を明るく短い光が上から下へ流れていきます。押せば次のセクションまでなめらかにスクロールして、ボタンは消えます。先頭の近くまで戻ってくると、ボタンがまた現れて光が流れます。

光の正体は、下の端だけが白いグラデーションの短い線です。線の要素に overflow: hidden を付けた中を、上の外から下の外まで1.6秒の power1.inOut で通しているので、線に差し込んで抜けていくように見えます。動き続けるものは読む人の気を散らすので、流すのは表示されたときの2回だけにして、5秒を越えて動かないようにしました。止まったあとでも、カーソルを重ねたりキーボードでフォーカスしたりすると、もう一度流れます。

スクロールは ScrollToPlugin で次のセクションの上端まで運びますが、途中でホイールや指で動かされたら、autoKill でその場で止めます。読む人が動かしているのに、自動のスクロールが引き戻すようなことはしたくありません。ボタンが消えるのは、ヒーローの高さの10%を越えてスクロールしたとき。その境目を越えたか戻ったかは ScrollTrigger で見ています。

ボタンの中身は次のセクションへのページ内リンクなので、スクリプトが動かなくても、ふつうのリンクとして移動できます。線と英字は飾りとしてスクリーンリーダーから外し、名前は aria-label で「次のセクションへ」としました。押すとボタン自体が消えてしまうので、スクロールし終えたところでフォーカスを次のセクションへ移しています。

コード

HTML
<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>

CSS
.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); }
}

JavaScript
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();
});

インタラクションデザインについて、
お気軽にご相談ください。

ここに並べているのは、Web サイトの中で動く UI のサンプルです。新しく作るサイトに組み込むことも、いま公開中のサイトに、ボタンやヒーロー、スクロールの演出だけを足すこともできます。設計から実装までの進め方はWeb サイト制作、端末ごとの見え方や表示速度の考え方はレスポンシブウェブサイトのページにまとめています。映像として作る動きをお探しの場合はモーショングラフィックスをご覧ください。