デモ
デモを読み込めませんでした。
仕様
- 用途
- グラフ
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
左の月から順に、棒が下から伸びます。棒の上の数字も 0 から同じ速さで数え上がるので、伸びきる瞬間に高さと数字が同時にそろいます。いちばん高い月だけ差し色にして、どこが山かをひと目で分かるようにしています。
1 本が伸びるのは 0.9 秒の power3.out、隣の棒とは 0.08 秒ずらします。この幅を広げると 8 本ぶんの遅れが積み上がり、最後の月がいつまでも出てきません。数字は棒より 0.15 秒遅れて、8px 下から浮き上がります。棒と同時に出すと、まだ伸びている先端に数字が付いていけません。
グラフは、絵だけでは読み上げに何も届きません。月と値はそのまま文字としてページに書き、棒の高さはその値(--value)から CSS で出します。数字を持つ場所が 1 つになるので、あとから値を直しても棒とずれません。
動かすのは、画面に入った 1 回だけにします。スクロールのたびに数え直すと、読み手が値を追えません。伸ばし終わったら、動きのために書き込んだ高さは消して CSS に返します。残したままだと、画面の幅が変わっても棒が最初の高さのままになります。動きを減らす設定では、伸びきった状態で置きます。
目盛りの上端・棒の幅・色は、まとめて CSS の値で変えられます。上端を実際の最大値ぴったりにすると、いちばん高い棒が天井に貼り付くので、少し余裕を取ります。売上や実績のように、月ごとの差を見せたいセクションに向いています。
コード
<figure class="chart">
<figcaption class="chart__caption">
<span class="chart__title">Revenue by month</span>
<span class="chart__note">2026年1月〜8月・単位:万円</span>
</figcaption>
<!-- 月と値は文字として置く。棒の高さは --value から CSS が出す -->
<ol class="chart__bars">
<li class="chart__item" style="--value: 380">
<span class="chart__month">1月</span><span class="chart__value">380</span><span class="chart__bar"></span>
</li>
<li class="chart__item" style="--value: 520">
<span class="chart__month">2月</span><span class="chart__value">520</span><span class="chart__bar"></span>
</li>
<li class="chart__item" style="--value: 450">
<span class="chart__month">3月</span><span class="chart__value">450</span><span class="chart__bar"></span>
</li>
<li class="chart__item" style="--value: 610">
<span class="chart__month">4月</span><span class="chart__value">610</span><span class="chart__bar"></span>
</li>
<li class="chart__item" style="--value: 570">
<span class="chart__month">5月</span><span class="chart__value">570</span><span class="chart__bar"></span>
</li>
<li class="chart__item" style="--value: 740">
<span class="chart__month">6月</span><span class="chart__value">740</span><span class="chart__bar"></span>
</li>
<li class="chart__item" style="--value: 680">
<span class="chart__month">7月</span><span class="chart__value">680</span><span class="chart__bar"></span>
</li>
<li class="chart__item chart__item--best" style="--value: 880">
<span class="chart__month">8月</span><span class="chart__value">880</span><span class="chart__bar"></span>
</li>
</ol>
</figure>
.chart {
--max: 1000; /* 目盛りの上端。棒の高さはこれに対する割合 */
--plot: 320px; /* 上端まで伸びたときの高さ */
--width: 52px; /* 棒の幅 */
--bar: #22375a; /* 棒の色 */
--best: #ff6b35; /* いちばん高い月の色 */
--ink: #262626; /* 文字の色 */
--mute: #6b6b6b; /* 月と注記の色 */
--line: rgba(38, 38, 38, 0.12); /* 目盛りの線の色 */
--surface: #ffffff;
margin: 0;
padding: 32px;
background: var(--surface);
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.chart__caption {
display: grid;
gap: 6px;
margin-bottom: 40px;
}
.chart__title {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.01em;
}
.chart__note {
font-size: 14px;
letter-spacing: 0.04em;
color: var(--mute);
}
/* 列の間は空けない。月のラベルの上の罫線が、そのまま 1 本の目盛りになる */
.chart__bars {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
align-items: end;
margin: 0;
padding: 0;
list-style: none;
}
/* 読み上げは「月 → 値」の順。見た目は order で 値 → 棒 → 月 に並べ替える */
.chart__item {
display: flex;
flex-direction: column;
align-items: center;
}
.chart__value {
order: 1;
margin-bottom: 8px;
font-size: 15px;
font-weight: 700;
}
.chart__bar {
order: 2;
flex: none;
width: min(var(--width), 76%);
height: calc(var(--value) / var(--max) * var(--plot));
border-radius: 4px 4px 0 0;
background: var(--bar);
}
.chart__month {
order: 3;
align-self: stretch;
padding-top: 10px;
border-top: 2px solid var(--line);
font-size: 14px;
letter-spacing: 0.04em;
text-align: center;
color: var(--mute);
}
/* いちばん高い月だけ差し色にする */
.chart__item--best {
--bar: var(--best);
color: var(--best);
}
import { gsap } from "gsap";
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
document.querySelectorAll(".chart").forEach((chart) => {
const items = [...chart.querySelectorAll(".chart__item")];
const bars = items.map((item) => item.querySelector(".chart__bar"));
const labels = items.map((item) => item.querySelector(".chart__value"));
// 値は CSS 変数に書いたものを正とする(棒の高さと数字が別々にならないように)
const values = items.map((item) => Number(item.style.getPropertyValue("--value")));
const counts = values.map(() => ({ v: 0 }));
const count = () => {
labels.forEach((label, i) => {
label.textContent = Math.round(counts[i].v).toLocaleString("ja-JP");
});
};
const grow = gsap
.timeline({ paused: true, onUpdate: count })
// 伸びきった高さは CSS が持っている。0 からその高さへ戻し、終わったら height を消して CSS に返す
.from(bars, { height: 0, duration: 0.9, ease: "power3.out", stagger: 0.08, clearProps: "height" }, 0)
.fromTo(counts, { v: 0 }, { v: (i) => values[i], duration: 0.9, ease: "power3.out", stagger: 0.08 }, 0)
// 数字は棒に少し遅れて、下から浮き上がる
.from(labels, { autoAlpha: 0, y: 8, duration: 0.35, ease: "power1.out", stagger: 0.08 }, 0.15);
// 動きを減らす設定では、伸びきった状態で見せる
if (reduceMotion) {
grow.progress(1);
return;
}
// 画面に入ったら 1 回だけ伸ばす
const io = new IntersectionObserver(
(entries, self) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
self.unobserve(entry.target);
grow.play();
}
},
{ rootMargin: "0px 0px -20%" } // 下から 20% 入ったら始める
);
io.observe(chart);
});