円形プログレスバー css スタットリング9選 — 数値も実際に動く
円形プログレスバー css とは、棒の代わりに円に沿って値の分だけ埋めていく進捗表示で、SVGの円ひとつに stroke-dasharray と stroke-dashoffset の二つを与えるだけで作れます。九つとも、ボタンやスライダーを操作すると値が引き直され、リングや線がその場で描き直されます。
自動再生 · タイルを押すとその項目へ · すべて1つのzip
- 01 3重 アクティビティリング
- 02 目標達成リング + 数値カウントアップ
- 03 セグメントリング(ステップ式)
- 04 グラデーションストロークリング
- 05 半円ゲージ + 針
- 06 リング内ミニスパークライン
- 07 リングバッジ(達成でチェックに変化)
- 08 比較リングペア(今週/先週)
- 09 リング進捗カードリスト
並び順は、リングが一本から複数へ、そして空のリングから中に何かを入れたリングへと進みます。同心円三本から始まり、リング一本に数字を添え、円を数えられる区画に区切り、線そのものに色を流し込み、半分に切って針を付けます。そのあとがリングの内側に折れ線を入れるもの、値が満ちた瞬間をチェックに変えるもの、二本を並べて比べるもの、最後が小さなリングをリストの各行に付けるものです。計算は九つとも同じで、変わるのは半径だけです。
| 半径 r | 円周 C = 2πr | 使っている項目 |
|---|---|---|
| 14 | 87.965 | 09 リスト行の小さなリング |
| 28 | 175.929 | 01 いちばん内側のリング |
| 36 | 226.195 | 08 比較リング2本 |
| 40 | 251.327 | 01 真ん中のリング |
| 44 | 276.460 | 03 セグメント・07 バッジ |
| 52 | 326.726 | 01 外側のリング・02・04・06 |
| 62 (半円) | 194.779 | 05 半円ゲージ |
013重 アクティビティリング
同心円三本がそれぞれ別の値まで巻かれ、「もう一度」を押すと三つの値が引き直されてリングと横のパーセントが一緒に変わります。歩数・運動・睡眠のように、指標三つを一枚のカードに重ねたいときに向いています。
.ar__ring--move { stroke-dasharray: 326.726; stroke-dashoffset: calc(326.726 * (1 - var(--p1) / 100)); }
.ar__ring--burn { stroke-dasharray: 251.327; stroke-dashoffset: calc(251.327 * (1 - var(--p2) / 100)); }
.ar__ring--rest { stroke-dasharray: 175.929; stroke-dashoffset: calc(175.929 * (1 - var(--p3) / 100)); }
@keyframes ar-wind {
0% { --p1: 0; --p2: 0; --p3: 0; }
45%, 55% { --p1: 82; --p2: 64; --p3: 45; }
100% { --p1: 0; --p2: 0; --p3: 0; }
}
02目標達成リング + 数値カウントアップ
リングが巻かれる間に中央の数字が0から1ずつ上がります。実際に動いている値は --num ひとつだけで、リングのオフセットも数字もそれを読むので、数字が72なのにリングは60で止まるというずれが起きません。
@property --num {
syntax: "<integer>";
inherits: true;
initial-value: 0;
}
.gr__ring {
stroke-dasharray: 326.726;
stroke-dashoffset: calc(326.726 * (1 - var(--num) / 100));
}
.gr__num::after {
counter-reset: n var(--num);
content: counter(n) "%";
}
03セグメントリング(ステップ式)
円ひとつを8区画に切っておき、「次へ」を押すたびに1つずつ点灯します。区画は長さで切り分けた8本の弧ではなく同じ円を8回重ねたもので、それぞれ破線1コマ分だけを塗り、オフセットで自分の位置まで押し出しています。
$C: 276.460; // 2π × 44
$slot: 34.5575; // C ÷ 8 = 1区画の取り分
$dash: 26; // そのうち塗る長さ。残りの 8.5575 が区画の隙間になる
.sg__seg {
stroke-dasharray: $dash ($C - $dash);
stroke-dashoffset: calc(var(--i) * -#{$slot});
opacity: .18;
}
.sg__seg.is-on { opacity: 1; }
04グラデーションストロークリング
リングの線そのものが空色からピンクへ変わり、スライダーを動かすとその値まで実際に埋まります。面ではなく線に色を入れるには、fill ではなく stroke に url(#id) でグラデーションを指定する必要があります。
// <linearGradient id="gdStroke" gradientUnits="userSpaceOnUse" x1="12" y1="12" x2="116" y2="116">
.gd__stop-a { stop-color: $color; } // 空色
.gd__stop-b { stop-color: $subject-pink; } // ピンク
.gd__ring {
stroke: url(#gdStroke);
stroke-width: 14;
stroke-linecap: round;
stroke-dasharray: 326.726;
stroke-dashoffset: calc(326.726 * (1 - var(--num) / 100));
}
05半円ゲージ + 針
弧が値の分だけ埋まると同時に、針が同じ角度まで回ります。半円の長さは円周の半分の πr なので半径62なら194.779で、針は0で左端・100で右端ですから、値1あたり1.8度ずつ回せば一致します。
.hg__arc {
stroke-dasharray: 194.779; // π × 62 = 半円の長さ
stroke-dashoffset: calc(194.779 * (1 - var(--num) / 100));
}
.hg__needle {
// 180度を100で割ると値1あたり1.8度
transform: rotate(calc(-90deg + var(--num) * 1.8deg));
transform-origin: 76px 76px;
}
06リング内ミニスパークライン
外側のリングが今月の進捗を巻く間に、内側で7日分の折れ線が左から描かれます。折れ線は座標が変われば実際の長さも変わりますが、pathLength="100" を与えると長さを100に固定して数えられるので、データがどう動いても同じ値で制御できます。
/* <polyline class="sp__line" pathLength="100" points="28,76 40,66 52,80 64,58 76,68 88,50 100,60"/> */
.sp__line { stroke-dasharray: 100; stroke-dashoffset: 0; }
@keyframes sp-draw {
0% { stroke-dashoffset: 100; }
45%, 55% { stroke-dashoffset: 0; }
100% { stroke-dashoffset: 100; }
}
// リングだけ12時から始まるように回す。内側の折れ線まで一緒に回ると倒れる
.sp__rings { transform: rotate(-90deg); transform-origin: 64px 64px; }
07リングバッジ(達成でチェックに変化)
「完了にする」を押すとリングが最後まで巻かれ、中央のパーセントが消えて、その場所にチェックが線をなぞって描かれます。このチェックはアイコンのグリフではなく頂点3つの polyline なので、リングとまったく同じオフセットのやり方が使えます。
.bg__check {
stroke-dasharray: 100;
stroke-dashoffset: 100;
transition: stroke-dashoffset $dur-enter $easing;
}
.bg.is-done .bg__check { stroke-dashoffset: 0; }
.bg.is-done .bg__num { opacity: 0; }
@keyframes bg-draw {
0%, 35% { stroke-dashoffset: 100; }
50%, 62% { stroke-dashoffset: 0; }
72%, 100% { stroke-dashoffset: 100; }
}
08比較リングペア(今週/先週)
同じ半径のリングを2本並べると、数値の差がそのまま角度の差になるので、ラベルを読む前に目が先に気づきます。「週を進める」を押すと今週の値が先週の位置へ移り、間の差分も符号と色が一緒に切り替わります。
.bp__cell--now .bp__ring { stroke-dashoffset: calc(226.195 * (1 - var(--a) / 100)); }
.bp__cell--prev .bp__ring { stroke-dashoffset: calc(226.195 * (1 - var(--b) / 100)); }
.bp__delta::after { counter-reset: d var(--d); content: "+" counter(d) "%"; }
.bp.is-down .bp__delta::after { content: "−" counter(d) "%"; }
.bp.is-down .bp__delta { background: $error-soft; color: $error; }
.bp.is-down .bp__ic { transform: scaleY(-1); }
09リング進捗カードリスト
小さなリング3つがリストの各行に付いて少しずつずれながら埋まり、行を押すとその行だけ10上がります。行ごとに目標が違うのでキーフレームも行ごとに必要ですが、SCSSの繰り返し一つで3組がまとめて生成されます。
$goals: 78, 52, 34;
@each $g in $goals {
$i: index($goals, $g);
.pl.is-demo .pl__row:nth-child(#{$i}) {
animation: pl-fill-#{$i} $dur-loop $easing infinite;
animation-delay: #{($i - 1) * 0.12}s;
}
@keyframes pl-fill-#{$i} {
0% { --v: 0; }
45%, 55% { --v: #{$g}; }
100% { --v: 0; }
}
}
どこで壊れるか — 落とし穴
九つを作っていていちばん長く手が止まったのは、カスタムプロパティを登録していなかったことでした。01の最初の版は --p をインラインスタイルに置いたまま、キーフレームの中で calc(var(--c) * (1 - var(--p))) と書いていたのですが、レンダリング計測では24枚のうち動いたフレームが2枚しかありませんでした。登録していないカスタムプロパティには型がないので、ブラウザは二つの値の間を補間できず、途中がないまま45%の地点で一度だけ切り替わって終わっていたわけです。@property で <integer> と宣言したところ、同じデモが18枚まで上がりました。巻かれずに飛ぶリングを見ているなら、まず値の型を疑うのが近道です。二つめによくずれるのは stroke-linecap: round です。丸い端は線の太さの半分だけ両側にはみ出すため、値が0でも点が一つ残り、100では始点と終点が重なって少し太く見えます。三つめはループの周期です。フレームは2秒の区間で撮るので、アニメーションを3秒にすると最後のフレームと最初のフレームが合わず1周ごとに飛んで見えます。そこで巻く区間と戻す区間を1周の中に入れ、必ず元の位置に帰ってくるようにしました。この三つをすべて反映した九つは、ダウンロード用パスワード zg9hdbsu を入れると開き、React 版にも同じ登録宣言がそのまま入っています。
アクセシビリティ
九つとも prefers-reduced-motion: reduce では動きだけを止めて値は残します。01・02・04・05・08 は自動ループを止めて既定の値のまま止まり、03 は8区画のうち5つが点いた状態で固定されます。06・07・09 はループのアニメーションを切りますが、クリックしたときの状態変化はそのまま残します。中央に数字を大きく出す7つ(02・03・04・05・07・08・09)にはその数字要素に role="status" を、01・06 にはリングの図全体に role="img" と aria-label を付けてあるので、リングが見えない人にも値の変化が読み上げられます。画面のパーセントはリングと同じ変数から counter() で出しているため、絵と読み上げが食い違うことはありません。
FAQ
conic-gradient で作る円形の進捗表示と何が違いますか?
conic-gradient は円盤を扇形に塗る方式なので、ドーナツにするには中央をもう一度覆う必要があり、端を丸くするのも面倒です。SVGの円と stroke-dashoffset は線を一本切って使う方式なので、太さも丸い端も重ね合わせも自然で、値の指定も長さの計算ひとつで済みます。
円周を自分で計算せずJavaScriptで求めてもいいですか?
構いません。circle.getTotalLength() が同じ値を返すので、半径を変えてもCSSを直さずに済みます。ただしこの九つは半径が固定なので、計算した数値をそのまま書き、自動ループだけはJavaScriptなしで回るようにしてあります。詳しくは MDN の stroke-dasharray にまとまっています。
リングが3時の方向から始まります。12時にするには?
SVGの円の0度は右です。リングを包むグループに transform: rotate(-90deg) を与え、円の中心を transform-origin に指定すれば12時から始まります。06のようにリングの内側に別の絵があると一緒に回ってしまうので、リングだけを別のグループにまとめてください。
画面全体を組んでいるところなら、ダッシュボード コンポーネント9選にヘッダー・カンバン・コマンドパレットが、ダッシュボード サイドバー・ウィジェット UI 9選にサイドバー・テーブル・空の状態があります。カスタムプロパティの登録は MDN の @property がいちばん短い説明です。