本文へスキップ
HELLO!
HIGASHIYUKI®.COM
0   /   100
セクション

スクロールでセクションのリストの行が、罫線を引きながら左から順に滑り込む

デモ

仕様

動作
DOM
種類
レシピ
動作確認
three 0.186.0・gsap 3.15.0
公開日

しくみと調整

行の上の罫線と、行の中身を分けて動かします。行が画面に入ったら、罫線を scaleX で左から引き、少し遅れて中身を 80px 左から寄せながら浮かべます。同じころに画面へ入った行はひとまとめにして、上の行から 0.08 秒ずつずらすので、線と文字が上から順に左から流れ込んできます。

罫線は 1 秒の power3.inOut、中身は 0.1 秒遅れて 0.9 秒の power3.out で動かします。線を先に引き始めると、引かれた線の上に文字が乗ってくるように見えます。出し始めるのは、行の上端が画面の上から 85% の高さに届いたときです。画面の下端で始めると、いちばん動く所が画面の外で終わってしまいます。

1 行ずつ結ぶのではなく、ScrollTrigger.batch でまとめて扱うのが要点です。1 行ずつだと、速くスクロールしたときに何行もばらばらに動き出し、順に流れ込む感じが消えます。出すのは 1 回だけ(once)にして、上へ戻したときは何もしません。

中身は opacity だけで隠し、読み上げとキーボードの移動から外さないようにします。Tab で画面の下の行へ移ったときは、線を越えるのを待たずに、その行を出します。罫線は行の疑似要素に描き、引いた割合を CSS 変数で持たせているので、行ごとに線の要素を足さずに済みます。

つまみは、寄せる距離、ずらす間隔、線と中身の時間の差です。寄せる距離を大きくすると勢いが出ますが、読み始めるまでの間が延びます。動きを減らす設定では、最初から出ている状態で見せます。サービスの一覧、料金に含まれる項目、よくある質問のように、同じ形の行が縦に続く節に向いています。

コード

HTML
<section class="services" aria-labelledby="services-title">
  <div class="services__intro">
    <div>
      <p class="services__label">Services</p>
      <h2 class="services__title" id="services-title">サービス</h2>
    </div>
    <p class="services__lead">ここに説明文が入ります。</p>
  </div>

  <!-- 行はいくつ増やしても、つなぎ方は変わらない -->
  <ol class="services__list">
    <li class="service">
      <a class="service__link" href="/services/brand-strategy/">
        <span class="service__number" aria-hidden="true">01</span>
        <span class="service__name">ブランド戦略</span>
        <span class="service__detail">調査・ワークショップ</span>
        <span class="service__arrow" aria-hidden="true">↗</span>
      </a>
    </li>
    <li class="service">
      <a class="service__link" href="/services/visual-identity/">
        <span class="service__number" aria-hidden="true">02</span>
        <span class="service__name">ビジュアル設計</span>
        <span class="service__detail">ロゴ・書体・色</span>
        <span class="service__arrow" aria-hidden="true">↗</span>
      </a>
    </li>
    <li class="service">
      <a class="service__link" href="/services/website-design/">
        <span class="service__number" aria-hidden="true">03</span>
        <span class="service__name">ウェブサイト制作</span>
        <span class="service__detail">UX・UI・プロトタイプ</span>
        <span class="service__arrow" aria-hidden="true">↗</span>
      </a>
    </li>
    <li class="service">
      <a class="service__link" href="/services/motion-systems/">
        <span class="service__number" aria-hidden="true">04</span>
        <span class="service__name">モーション設計</span>
        <span class="service__detail">動きの原則・ライブラリ</span>
        <span class="service__arrow" aria-hidden="true">↗</span>
      </a>
    </li>
    <li class="service">
      <a class="service__link" href="/services/product-interfaces/">
        <span class="service__number" aria-hidden="true">05</span>
        <span class="service__name">アプリの画面設計</span>
        <span class="service__detail">デザインシステム・UI</span>
        <span class="service__arrow" aria-hidden="true">↗</span>
      </a>
    </li>
    <li class="service">
      <a class="service__link" href="/services/content/">
        <span class="service__number" aria-hidden="true">06</span>
        <span class="service__name">コンテンツ制作</span>
        <span class="service__detail">ことばのトーン・編集</span>
        <span class="service__arrow" aria-hidden="true">↗</span>
      </a>
    </li>
    <li class="service">
      <a class="service__link" href="/services/launch-support/">
        <span class="service__number" aria-hidden="true">07</span>
        <span class="service__name">公開のサポート</span>
        <span class="service__detail">キャンペーン・制作物</span>
        <span class="service__arrow" aria-hidden="true">↗</span>
      </a>
    </li>
  </ol>
</section>

CSS
.services {
  --ink: #eeece6;
  --mute: #a8a6a0;
  --rule: rgba(238, 236, 230, 0.14); /* 行の上の罫線 */
  padding: 96px clamp(24px, 7vw, 96px);
  background-color: #111111;
  color: var(--ink);
}

/* 見出しとリード文は、最後の行の文字の並びでそろえる */
.services__intro {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: last baseline;
  gap: 24px 48px;
  margin-bottom: 48px;
}

.services__label { margin: 0 0 16px; font-size: 13px; font-weight: 700; line-height: 1; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute); }
.services__title { margin: 0; font-size: clamp(44px, 6vw, 72px); font-weight: 800; line-height: 1.05; letter-spacing: -0.02em; }
.services__lead { margin: 0; font-size: 18px; line-height: 1.8; letter-spacing: 0.04em; word-break: auto-phrase; }

.services__list { margin: 0; padding: 0; list-style: none; }

/* 罫線は疑似要素に描き、--draw(0〜1)で左から引く。JS が動かないときは引いたまま */
.service { position: relative; }

.service::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1px;
  background-color: var(--rule);
  transform: scaleX(var(--draw, 1));
  transform-origin: 0 50%;
}

.service__link {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr) auto auto;
  align-items: center;
  column-gap: 24px;
  padding: 24px 0;
  color: inherit;
  text-decoration: none;
}

.service__link:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

.service__number { font-size: 14px; letter-spacing: 0.04em; color: var(--mute); font-variant-numeric: tabular-nums; }
.service__name { font-size: clamp(24px, 3.4vw, 44px); font-weight: 700; line-height: 1.1; letter-spacing: -0.02em; }
.service__detail { font-size: 14px; letter-spacing: 0.04em; color: var(--mute); }
.service__arrow { font-size: 24px; line-height: 1; }

/* 狭い画面では、内容を名前の下へ回す */
@media (max-width: 720px) {
  .service__link { grid-template-columns: 40px minmax(0, 1fr) auto; row-gap: 8px; }
  .service__detail { grid-column: 2; grid-row: 2; }
  .service__arrow { grid-column: 3; grid-row: 1; }
}

JavaScript
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

const rows = gsap.utils.toArray(".service");
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

// 動きを減らす設定では何もしない(行は最初から出ている状態のまま)
if (rows.length > 0 && !reduceMotion) {
  const shown = new Set();
  const inner = (list) => list.map((row) => row.querySelector(".service__link"));

  // まだ出していない行だけを、並びの順に 0.08 秒ずつずらして出す。線を先に引き始め、中身は 0.1 秒遅れて左から寄せる
  const reveal = (batch) => {
    const fresh = batch.filter((row) => !shown.has(row));
    if (fresh.length === 0) return;
    fresh.forEach((row) => shown.add(row));
    gsap.to(fresh, { "--draw": 1, duration: 1, ease: "power3.inOut", stagger: 0.08 });
    gsap.to(inner(fresh), { x: 0, opacity: 1, duration: 0.9, ease: "power3.out", stagger: 0.08, delay: 0.1 });
  };

  // 中身は opacity だけで隠す。visibility で隠すと、出る前の行が読み上げとタブ移動から外れる
  gsap.set(rows, { "--draw": 0 });
  gsap.set(inner(rows), { x: -80, opacity: 0 });

  // 同じころに画面へ入った行をまとめて扱う。1 行ずつ結ぶと、速く送ったときにばらばらに動き出す
  ScrollTrigger.batch(rows, { start: "top 85%", once: true, onEnter: reveal });

  // Tab で画面の下の行へ移ったときは、線を越えるのを待たずに出す
  document.addEventListener("focusin", (event) => {
    const row = event.target.closest?.(".service");
    if (rows.includes(row)) reveal([row]);
  });
}

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

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