デモ
デモを読み込めませんでした。
仕様
- 用途
- グラフ
- 動作
- DOM
- 種類
- レシピ
- 動作確認
- three 0.186.0・gsap 3.15.0
- 公開日
しくみと調整
左の月から順に棒が伸び、上に載った数字も0から数え上がって、伸びきった瞬間に高さと数字がぴたりとそろいます。いちばん高い月の棒だけは濃い色にして、山の位置がひと目で分かるようにしました。差は色相ではなく明るさで付けているので、色の見え方が違っても山は見分けられます。
棒は0.9秒の power3.out で、隣とは少しずつ開始をずらしています。このずれを広げると8本ぶんの遅れが積み重なり、最後の月がなかなか伸び始めません。数字は棒のすぐ上に並んだ要素なので、少し遅れて下から浮かび上がったあとは、伸びていく棒に押し上げられながら一緒に上がります。
値の置き場所はひとつです。月と値は文字としてページに書き、棒の高さはその値(--value)から CSS で計算しています。スクリーンリーダーには文字がそのまま届き、あとで値を直しても棒と数字はずれません。伸ばし終えたら、アニメーションのために書き込んだ高さを消して CSS の計算に返します。残したままだと、あとから CSS の値を変えても、棒は最初に伸ばした高さから動かなくなります。
動かすのは画面に入ったときの一度だけで、スクロールのたびに数え直すことはしません。それでは読む人が値を追えなくなります。目盛りの上端は、実際の最大値ぴったりではなく少し余裕を持たせてください。ぴったりにすると、いちばん高い棒がグラフの天井に貼り付いてしまいます。
コード
<figure class="chart">
<figcaption class="chart__caption">
<span class="chart__title">月別の売上</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: #a28767; /* 棒の色 */
--best: #4a5b66; /* いちばん高い月の色(明るさの差で目立たせる) */
--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);
}
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);
});