デモ
デモを読み込めませんでした。
仕様
- 用途
- セクション
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
企業や店舗のサイトによくある「お知らせ」の一覧です。一覧が画面に入ると、行が上から 0.08 秒ずつずれて現れます。行ごとに、下の罫線を左から引き、日付と見出しは行の中の窓(はみ出しを隠す箱)の下からせり上げ、区分の札は clip-path で左から開きます。最後に右の矢印が少し左から出てきます。
罫線は 0.9 秒の power3.inOut で引き、日付は 0.12 秒、札は 0.2 秒、見出しは 0.24 秒遅れて始めます。左の日付から右の見出しへ順に追いかけるので、1 行の中でも読む順に目が送られます。出すのは一覧が画面の下から 15% の線を越えた 1 回だけなので、スクロールを追い続ける仕組みは要らず、IntersectionObserver で足ります。
上のタブで区分を選ぶと、今の行を 0.25 秒で薄れさせてから、合わない行に hidden を付けて詰め、合う行だけを同じ出方でもう一度出します。出方のタイムラインは、消し終えた onComplete の中で作ります。先に作ると、作った時点で出る前の形が描かれ、消えている途中の行がいきなり隠れます。消える途中で別のタブを押しても、今の濃さから消し直します。押したタブは aria-pressed で示し、件数を読み上げ用の領域に書きます。
行に重ねたときの見た目は、0〜1 の CSS 変数 --grow ひとつで決めます。行の地を白へ寄せ、矢印の軸を 16px から 32px へ伸ばし、見出しを 8px 右へずらす量を、CSS の側でこの変数から計算します。GSAP はこの変数だけを 0.3 秒で動かし、離れたら 0.15 秒で戻します。Tab で行へ移ったときも同じように伸びます。
スクロールで行を横から滑り込ませる一覧や、カード全体に下線を引く記事カードと違い、1 行の中の日付・札・見出しを順に追わせ、区分で絞り込み直せるのが持ち味です。つまみは、行どうしの間隔、罫線と中身の時間の差、札の地の色(CSS の変数)です。動きを減らす設定では、最初から出しきった形で見せ、絞り込みも一度に切り替えます。お知らせやプレスリリースの一覧、イベントの告知に向いています。
コード
<!-- 区分は、行の data-kind とタブの data-filter の値でそろえる(all はすべて) -->
<section class="news" aria-labelledby="news-title">
<div class="news__head">
<p class="news__eyebrow">News</p>
<h2 class="news__title" id="news-title">お知らせ</h2>
<p class="news__lead">営業日のご案内や、<br>新しい取り組みをお知らせします。</p>
<a class="news__more" href="/news/">一覧を見る<span aria-hidden="true">→</span></a>
</div>
<div class="news__body">
<div class="news__tabs" role="group" aria-label="区分で絞り込む">
<button type="button" data-filter="all" aria-pressed="true">すべて</button>
<button type="button" data-filter="notice" aria-pressed="false">お知らせ</button>
<button type="button" data-filter="press" aria-pressed="false">プレスリリース</button>
<button type="button" data-filter="event" aria-pressed="false">イベント</button>
</div>
<p class="news__status" role="status" data-template="{label}:{count}件"></p>
<ul class="news__list">
<li class="news__item" data-kind="notice">
<a class="news__link" href="/news/autumn-holiday/">
<span class="news__mask"><time class="news__rise" datetime="2026-09-30">2026.09.30</time></span>
<span class="news__tag">お知らせ</span>
<span class="news__mask news__heading"><span class="news__rise">秋季休業日のお知らせ</span></span>
<span class="news__arrow" aria-hidden="true"><svg viewBox="0 0 8 12" width="8" height="12"><path d="M1.5 1.5L6 6l-4.5 4.5"/></svg></span>
</a>
</li>
<li class="news__item" data-kind="press">
<a class="news__link" href="/news/online-desk/">
<span class="news__mask"><time class="news__rise" datetime="2026-09-24">2026.09.24</time></span>
<span class="news__tag">プレスリリース</span>
<span class="news__mask news__heading"><span class="news__rise">新しい相談窓口をオンラインで開設しました</span></span>
<span class="news__arrow" aria-hidden="true"><svg viewBox="0 0 8 12" width="8" height="12"><path d="M1.5 1.5L6 6l-4.5 4.5"/></svg></span>
</a>
</li>
<li class="news__item" data-kind="event">
<a class="news__link" href="/news/fukuoka-meetup/">
<span class="news__mask"><time class="news__rise" datetime="2026-09-18">2026.09.18</time></span>
<span class="news__tag">イベント</span>
<span class="news__mask news__heading"><span class="news__rise">10月12日に、福岡で制作の相談会を開きます</span></span>
<span class="news__arrow" aria-hidden="true"><svg viewBox="0 0 8 12" width="8" height="12"><path d="M1.5 1.5L6 6l-4.5 4.5"/></svg></span>
</a>
</li>
<li class="news__item" data-kind="notice">
<a class="news__link" href="/news/renewal/">
<span class="news__mask"><time class="news__rise" datetime="2026-09-09">2026.09.09</time></span>
<span class="news__tag">お知らせ</span>
<span class="news__mask news__heading"><span class="news__rise">ホームページを新しくしました</span></span>
<span class="news__arrow" aria-hidden="true"><svg viewBox="0 0 8 12" width="8" height="12"><path d="M1.5 1.5L6 6l-4.5 4.5"/></svg></span>
</a>
</li>
<li class="news__item" data-kind="press">
<a class="news__link" href="/news/casebook-2026/">
<span class="news__mask"><time class="news__rise" datetime="2026-08-28">2026.08.28</time></span>
<span class="news__tag">プレスリリース</span>
<span class="news__mask news__heading"><span class="news__rise">制作事例集の2026年版を公開しました</span></span>
<span class="news__arrow" aria-hidden="true"><svg viewBox="0 0 8 12" width="8" height="12"><path d="M1.5 1.5L6 6l-4.5 4.5"/></svg></span>
</a>
</li>
<li class="news__item" data-kind="event">
<a class="news__link" href="/news/writing-seminar/">
<span class="news__mask"><time class="news__rise" datetime="2026-08-19">2026.08.19</time></span>
<span class="news__tag">イベント</span>
<span class="news__mask news__heading"><span class="news__rise">勉強会「伝わる文章」の録画を公開しました</span></span>
<span class="news__arrow" aria-hidden="true"><svg viewBox="0 0 8 12" width="8" height="12"><path d="M1.5 1.5L6 6l-4.5 4.5"/></svg></span>
</a>
</li>
</ul>
</div>
</section>
.news {
--ink: #262626;
--mute-small: #6b6b6b;
--line: rgba(38, 38, 38, 0.12);
--hover: 255 255 255; /* 重ねたときの行の地(rgb の数字) */
--tag-notice: #cdd2d3; /* 札の地(中身の色を地に 28% 混ぜた淡い色) */
--tag-press: #d0d5cc;
--tag-event: #dcd4c7;
display: grid;
grid-template-columns: 272px minmax(0, 1fr);
gap: 48px;
padding: 96px max(24px, calc((100% - 1088px) / 2));
background: #f2f1ed;
color: var(--ink);
}
.news__eyebrow { margin: 0; font-size: 13px; font-weight: 700; line-height: 1; letter-spacing: 0.14em; text-transform: uppercase; color: var(--mute-small); }
.news__title { margin: 16px 0 0; font-size: 44px; font-weight: 800; line-height: 1.1; letter-spacing: -0.02em; }
.news__lead { margin: 24px 0 0; font-size: 14px; line-height: 1.8; letter-spacing: 0.04em; color: var(--mute-small); word-break: auto-phrase; }
.news__more { display: inline-flex; align-items: center; gap: 8px; height: 48px; margin-top: 32px; padding: 0 28px; box-sizing: border-box; border: 1px solid var(--ink); border-radius: 999px; color: inherit; font-size: 14px; font-weight: 600; letter-spacing: 0.04em; text-decoration: none; }
.news__tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 24px; }
.news__tabs button {
height: 36px;
padding: 0 18px;
border: 1px solid var(--line);
border-radius: 999px;
background: transparent;
color: var(--ink);
font: inherit;
font-size: 14px;
font-weight: 600;
letter-spacing: 0.04em;
cursor: pointer;
transition: background-color 0.2s, color 0.2s, border-color 0.2s;
}
.news__tabs [aria-pressed="true"] { border-color: var(--ink); background: var(--ink); color: #ffffff; }
.news__tabs button:focus-visible,
.news__link:focus-visible { outline: 2px solid var(--ink); outline-offset: -2px; }
.news__status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.news__list { margin: 0; padding: 0; list-style: none; border-top: 1px solid var(--line); }
.news__item[hidden] { display: none; }
/* 下の罫線。引いた割合(--draw)を GSAP で動かす */
.news__item { --draw: 1; position: relative; }
.news__item::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--line); transform: scaleX(var(--draw)); transform-origin: 0 50%; }
/* 重ねた量(--grow: 0〜1)で、地・矢印の長さ・見出しのずれを決める */
.news__link {
--grow: 0;
position: relative;
display: flex;
align-items: center;
height: 76px;
padding: 0 24px;
box-sizing: border-box;
background: rgb(var(--hover) / calc(var(--grow) * 0.9));
color: inherit;
text-decoration: none;
}
.news__mask { display: block; flex: none; height: 26px; overflow: hidden; }
.news__rise { display: block; line-height: 26px; }
.news__mask:first-child { width: 96px; font-size: 14px; letter-spacing: 0.04em; color: var(--mute-small); font-variant-numeric: tabular-nums; }
.news__tag { flex: none; width: 112px; margin-left: 16px; padding: 6px 0; border-radius: 2px; background: var(--tag-notice); font-size: 12px; font-weight: 600; line-height: 1; letter-spacing: 0.06em; text-align: center; }
[data-kind="press"] .news__tag { background: var(--tag-press); }
[data-kind="event"] .news__tag { background: var(--tag-event); }
.news__heading { flex: 1; min-width: 0; margin: 0 56px 0 24px; font-size: 16px; letter-spacing: 0.02em; translate: calc(var(--grow) * 8px) 0; }
.news__heading .news__rise { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.news__arrow { position: absolute; right: 32px; top: 50%; width: 40px; height: 12px; margin-top: -6px; }
.news__arrow::before { content: ""; position: absolute; left: 0; top: 5px; width: calc(16px + var(--grow) * 16px); height: 2px; background: var(--ink); }
.news__arrow svg { position: absolute; left: -7px; top: 0; overflow: visible; translate: calc(16px + var(--grow) * 16px) 0; fill: none; stroke: var(--ink); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
/* 狭い画面では、見出しの列を上に積み、行の見出しを日付と札の下へ折り返す */
@media (max-width: 800px) {
.news { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 560px) {
.news__link { flex-wrap: wrap; height: auto; padding: 20px 72px 20px 16px; }
.news__heading { flex-basis: 100%; height: auto; margin: 8px 0 0; }
.news__heading .news__rise { white-space: normal; word-break: auto-phrase; line-height: 1.6; }
.news__arrow { right: 16px; }
}
import { gsap } from "gsap";
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const STAGGER = 0.08; // 行どうしの間隔(秒)
// 行の出方: 罫線を左から引き、日付と見出しを窓の下からせり上げ、札を左から開き、矢印を少し左から出す
function enter(items) {
const tl = gsap.timeline();
items.forEach((item, k) => {
const [date, heading] = item.querySelectorAll(".news__rise");
const s = k * STAGGER;
tl.fromTo(item, { "--draw": 0 }, { "--draw": 1, duration: 0.9, ease: "power3.inOut" }, s)
.fromTo(date, { yPercent: 100 }, { yPercent: 0, duration: 0.7, ease: "power3.out" }, s + 0.12)
.fromTo(item.querySelector(".news__tag"), { clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)", duration: 0.5, ease: "power2.out" }, s + 0.2)
.fromTo(heading, { yPercent: 100 }, { yPercent: 0, duration: 0.8, ease: "power3.out" }, s + 0.24)
.fromTo(item.querySelector(".news__arrow"), { x: -12, opacity: 0 }, { x: 0, opacity: 1, duration: 0.6, ease: "power3.out" }, s + 0.34);
});
// 動きを減らす設定では、出しきった形にする
return reduceMotion ? tl.progress(1) : tl;
}
document.querySelectorAll(".news").forEach((news) => {
const list = news.querySelector(".news__list");
const items = [...news.querySelectorAll(".news__item")];
const tabs = [...news.querySelectorAll("[data-filter]")];
const status = news.querySelector(".news__status");
let current = "all";
let exit = null;
// 最初の出方は、一覧が画面に入るまで止めておく(作った時点で、出る前の形になる)
let show = enter(items);
if (!reduceMotion) show.pause(0);
const io = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting) return;
io.disconnect();
show.play();
},
{ rootMargin: "0px 0px -15% 0px" }
);
io.observe(list);
// Tab で行へ移ったときは、画面に入るのを待たずに出しきる
list.addEventListener("focusin", () => {
io.disconnect();
if (show.paused()) show.progress(1);
});
// 絞り込み: 今の行を薄れさせて消し、合う行だけを並べ直して、同じ出方でもう一度出す
tabs.forEach((tab) => {
tab.addEventListener("click", () => {
const to = tab.dataset.filter;
if (to === current) return;
current = to;
tabs.forEach((other) => other.setAttribute("aria-pressed", String(other === tab)));
const next = items.filter((item) => to === "all" || item.dataset.kind === to);
status.textContent = status.dataset.template.replace("{label}", tab.textContent).replace("{count}", next.length);
io.disconnect();
exit?.kill();
show.kill();
const swap = () => {
items.forEach((item) => (item.hidden = !next.includes(item)));
gsap.set(items, { opacity: 1 });
show = enter(next);
};
if (reduceMotion) return swap();
// 消える途中で押し直しても、今の濃さから消す
exit = gsap.to(items.filter((item) => !item.hidden), { opacity: 0, duration: 0.25, ease: "power1.in", stagger: 0.03, onComplete: swap });
});
});
// 重ねたとき(キーボードの焦点も): 行の地・矢印の長さ・見出しのずれを --grow の 0〜1 で動かす
news.querySelectorAll(".news__link").forEach((link) => {
const grow = (on) => gsap.to(link, { "--grow": on ? 1 : 0, duration: reduceMotion ? 0 : on ? 0.3 : 0.15, ease: "power2.inOut", overwrite: true });
const leave = () => {
if (!link.matches(":hover") && !link.matches(":focus-visible")) grow(false);
};
link.addEventListener("pointerenter", () => grow(true));
link.addEventListener("pointerleave", leave);
link.addEventListener("focus", () => link.matches(":focus-visible") && grow(true));
link.addEventListener("blur", leave);
});
});