GODRICH

円形プログレスバー css スタットリング9選 — 数値も実際に動く

円形プログレスバー css とは、棒の代わりに円に沿って値の分だけ埋めていく進捗表示で、SVGの円ひとつに stroke-dasharray と stroke-dashoffset の二つを与えるだけで作れます。九つとも、ボタンやスライダーを操作すると値が引き直され、リングや線がその場で描き直されます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

並び順は、リングが一本から複数へ、そして空のリングから中に何かを入れたリングへと進みます。同心円三本から始まり、リング一本に数字を添え、円を数えられる区画に区切り、線そのものに色を流し込み、半分に切って針を付けます。そのあとがリングの内側に折れ線を入れるもの、値が満ちた瞬間をチェックに変えるもの、二本を並べて比べるもの、最後が小さなリングをリストの各行に付けるものです。計算は九つとも同じで、変わるのは半径だけです。

半径 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重 アクティビティリング

同心円三本がそれぞれ別の値まで巻かれ、「もう一度」を押すと三つの値が引き直されてリングと横のパーセントが一緒に変わります。歩数・運動・睡眠のように、指標三つを一枚のカードに重ねたいときに向いています。

stroke-dashoffset3 circlesclick
.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で止まるというずれが起きません。

@propertycounter()click
@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コマ分だけを塗り、オフセットで自分の位置まで押し出しています。

stroke-dasharrayrotateclick
$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 ではなく strokeurl(#id) でグラデーションを指定する必要があります。

linearGradientinput rangestroke-linecap
// <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度ずつ回せば一致します。

path arcrotateclick
.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に固定して数えられるので、データがどう動いても同じ値で制御できます。

polylinepathLengthclick
/* <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 なので、リングとまったく同じオフセットのやり方が使えます。

stroke-dashoffsetopacityclick
.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本並べると、数値の差がそのまま角度の差になるので、ラベルを読む前に目が先に気づきます。「週を進める」を押すと今週の値が先週の位置へ移り、間の差分も符号と色が一緒に切り替わります。

2 ringsdeltaclick
.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組がまとめて生成されます。

r=14animation-delayclick
$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 がいちばん短い説明です。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。