デモ
デモを読み込めませんでした。
仕様
- 用途
- セクション
- 動作
- DOM
- 種類
- レシピ
- 使うもの
- ScrollTrigger
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
年表の縦線は、先端が画面の中央にとどまったまま、スクロールしたぶんだけ下へ伸びていきます。先端が項目の点に届くと点が塗られ、その項目の本文が線の外側から寄ってきます。
縦線は2本重ねてあり、動かすのは上の線だけです。年表の上端が画面の中央に来たときから、下端が中央に来たときまでを線の伸びに比例させているので(scrub)、先端は中央から離れません。
線と項目とで、動かし方を分けました。線はスクロールに吸い付き、項目は決まった時間できびきび現れます。点は0.4秒の back.out(3) で少し弾ませて塗り、本文は0.05秒遅れて、40px寄せながら0.7秒で浮かび上がります。項目までスクロール量に比例させると、止めた位置によっては本文が半透明のまま残ってしまいます。
戻るときの動きも決めてあります。項目の toggleActions は「play none none reverse」で、上へ戻って点が画面の中央より下へ行けば、項目は引っ込みます。線が縮んでいるのに項目が出たままだと、線の先より下に項目だけが浮いて見えるからです。
線の位置と項目の間隔は CSS の変数で決めてあり、狭い画面では線を左へ寄せて、項目をすべて右側に並べます。本文は opacity だけで隠しているので、見えていない間もスクリーンリーダーやキーボードでたどれます。本文の中のリンクにフォーカスが移ったときは、その項目だけ先に表示します。
コード
<section class="history" aria-labelledby="history-title">
<p class="history__eyebrow">History</p>
<h2 class="history__title" id="history-title">12年の歩みを、<wbr>1本の線で。</h2>
<!-- 線は ol の ::before(下の線)と ::after(伸びる線)。点は各項目の上端に置く -->
<ol class="timeline">
<li class="timeline__item">
<span class="timeline__dot" aria-hidden="true"></span>
<div class="timeline__body">
<time class="timeline__date" datetime="2014-04">2014年4月</time>
<h3 class="timeline__name">2人で創業</h3>
<p class="timeline__text">ここに説明文が入ります。</p>
</div>
</li>
<li class="timeline__item">
<span class="timeline__dot" aria-hidden="true"></span>
<div class="timeline__body">
<time class="timeline__date" datetime="2016-10">2016年10月</time>
<h3 class="timeline__name">最初のプロダクト</h3>
<p class="timeline__text">ここに説明文が入ります。</p>
</div>
</li>
<li class="timeline__item">
<span class="timeline__dot" aria-hidden="true"></span>
<div class="timeline__body">
<time class="timeline__date" datetime="2018-06">2018年6月</time>
<h3 class="timeline__name">リモートワークに移行</h3>
<p class="timeline__text">ここに説明文が入ります。</p>
</div>
</li>
<li class="timeline__item">
<span class="timeline__dot" aria-hidden="true"></span>
<div class="timeline__body">
<time class="timeline__date" datetime="2020-02">2020年2月</time>
<h3 class="timeline__name">デザインシステム</h3>
<p class="timeline__text">ここに説明文が入ります。</p>
</div>
</li>
<li class="timeline__item">
<span class="timeline__dot" aria-hidden="true"></span>
<div class="timeline__body">
<time class="timeline__date" datetime="2023-09">2023年9月</time>
<h3 class="timeline__name">新しいスタジオ</h3>
<p class="timeline__text">ここに説明文が入ります。</p>
</div>
</li>
<li class="timeline__item">
<span class="timeline__dot" aria-hidden="true"></span>
<div class="timeline__body">
<time class="timeline__date" datetime="2026-01">2026年1月</time>
<h3 class="timeline__name">これから</h3>
<p class="timeline__text">ここに説明文が入ります。</p>
</div>
</li>
</ol>
</section>
.history {
--ink: #262626;
--muted: #6b6b6b;
--surface: #ffffff;
--line: rgba(38, 38, 38, 0.12);
--gap: 200px; /* 項目の間隔 */
--axis: 50%; /* 線の位置。狭い画面では左へ寄せる */
padding: 20vh 6vw 50vh;
background-color: #f2f1ed;
color: var(--ink);
}
.history__eyebrow {
margin: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.14em;
text-align: center;
text-transform: uppercase;
color: var(--muted);
}
.history__title {
margin: 16px 0 56px;
font-size: clamp(28px, 3.4vw, 44px);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.02em;
text-align: center;
word-break: keep-all; /* 狭い画面では、<wbr> の所(読点の後)で折り返す */
}
.timeline {
position: relative;
margin: 0;
padding: 64px 0 0;
list-style: none;
}
/* 線は 2 本重ねる。下の線(::before)と、スクリプトが --drawn で伸ばす線(::after) */
.timeline::before,
.timeline::after {
content: "";
position: absolute;
inset: 0 auto 0 calc(var(--axis) - 2px);
width: 4px;
border-radius: 4px;
background-color: var(--line);
}
.timeline::after {
background-color: var(--ink);
transform: scaleY(var(--drawn, 1));
transform-origin: 50% 0;
}
.timeline__item {
position: relative;
min-height: var(--gap);
}
.timeline__dot {
position: absolute;
top: -10px;
left: calc(var(--axis) - 10px);
width: 20px;
height: 20px;
box-sizing: border-box;
border: 2px solid var(--ink);
border-radius: 50%;
background-color: var(--ink);
}
/* 年月の行の中心を点の高さにそろえる(上の余白 24px + 年月の行の半分 7px だけ上げる) */
.timeline__body {
position: relative;
top: -31px;
width: 34%;
box-sizing: border-box;
margin-left: 12%;
padding: 24px;
border-radius: 4px;
background-color: var(--surface);
text-align: right;
}
.timeline__item:nth-child(even) .timeline__body { margin-left: 54%; text-align: left; }
.timeline__date { display: block; font-size: 14px; font-weight: 600; line-height: 1; color: var(--muted); }
.timeline__name { margin: 12px 0 0; font-size: 24px; line-height: 1.3; }
.timeline__text { margin: 8px 0 0; font-size: 14px; line-height: 1.8; color: var(--muted); word-break: auto-phrase; }
/* 列が並ばない幅では、線を左へ寄せ、項目をすべて右に置く */
@media (max-width: 720px) {
.history { --axis: 12px; }
.history .timeline__item .timeline__body { width: auto; margin-left: 40px; text-align: left; }
.timeline__name { font-size: 20px; }
}
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".timeline").forEach((timeline) => {
// 動きを減らす設定では、線を引き終えて項目を表示した、CSS のままの状態で見せる
if (reduceMotion) return;
// GSAP は var(--name) のままでは補間できないので、色は値にして渡す
const styles = getComputedStyle(timeline);
const ink = styles.getPropertyValue("--ink").trim();
const surface = styles.getPropertyValue("--surface").trim();
// 線: 年表の上端が画面の中央に来てから、下端が中央に来るまでを、スクロール量にそのまま比例させる。
// こうすると、線の先がいつも画面の中央にある
gsap.fromTo(
timeline,
{ "--drawn": 0 },
{
"--drawn": 1,
ease: "none",
scrollTrigger: { trigger: timeline, start: "top center", end: "bottom center", scrub: true },
}
);
// 項目: 線の先が点(項目の上端)に届いたら、時間で表示する。上へ戻して点が中央より下へ行ったら引っ込める
timeline.querySelectorAll(".timeline__item").forEach((item) => {
const dot = item.querySelector(".timeline__dot");
const body = item.querySelector(".timeline__body");
const from = body.offsetLeft > dot.offsetLeft ? 40 : -40; // 線の外側から寄せる
const reveal = gsap
.timeline({ scrollTrigger: { trigger: item, start: "top center", toggleActions: "play none none reverse" } })
.fromTo(dot, { scale: 0.6, backgroundColor: surface }, { scale: 1, backgroundColor: ink, duration: 0.4, ease: "back.out(3)" })
.fromTo(body, { opacity: 0, x: from }, { opacity: 1, x: 0, duration: 0.7, ease: "power3.out" }, 0.05);
// キーボードで中のリンクへ移ったときは、線が届くのを待たずに表示する
item.addEventListener("focusin", () => reveal.play());
});
});