GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

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

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

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

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

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

css プログレスバー 円として何が見えるか

同心円三本がそれぞれ別の値まで巻かれ、「もう一度」を押すと三つの値が引き直されてリングと横のパーセントが一緒に変わります。

3重 アクティビティリング
新しいタブで開く

01 · 3重 アクティビティリング — 計測 累積面積 7.207% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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ずつ上がります。

目標達成リング + 数値カウントアップ
新しいタブで開く

02 · 目標達成リング + 数値カウントアップ — 計測 累積面積 2.951% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

@propertycounter()click

コードの読み方

実際に動いている値は --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つずつ点灯します。

セグメントリング(ステップ式)
新しいタブで開く

03 · セグメントリング(ステップ式) — 計測 累積面積 1.895% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

stroke-dasharrayrotateclick

どこを変えるか

区画は長さで切り分けた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グラデーションストロークリング

何が見えるか

リングの線そのものが空色からピンクへ変わり、スライダーを動かすとその値まで実際に埋まります。

グラデーションストロークリング
新しいタブで開く

04 · グラデーションストロークリング — 計測 累積面積 3.247% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

linearGradientinput rangestroke-linecap

コードのどこを見るか

面ではなく線に色を入れるには、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半円ゲージ + 針

画面で起きること

弧が値の分だけ埋まると同時に、針が同じ角度まで回ります。

半円ゲージ + 針
新しいタブで開く

05 · 半円ゲージ + 針 — 計測 累積面積 3.285% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

path arcrotateclick

コードの読み方

半円の長さは円周の半分の π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日分の折れ線が左から描かれます。

リング内ミニスパークライン
新しいタブで開く

06 · リング内ミニスパークライン — 計測 累積面積 2.05% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

polylinepathLengthclick

どこを変えるか

折れ線は座標が変われば実際の長さも変わりますが、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リングバッジ(達成でチェックに変化)

何が見えるか

「完了にする」を押すとリングが最後まで巻かれ、中央のパーセントが消えて、その場所にチェックが線をなぞって描かれます。

リングバッジ(達成でチェックに変化)
新しいタブで開く

07 · リングバッジ(達成でチェックに変化) — 計測 累積面積 3.041% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

stroke-dashoffsetopacityclick

コードのどこを見るか

このチェックはアイコンのグリフではなく頂点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本並べると、数値の差がそのまま角度の差になるので、ラベルを読む前に目が先に気づきます。

比較リングペア(今週/先週)
新しいタブで開く

08 · 比較リングペア(今週/先週) — 計測 累積面積 4.356% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

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上がります。

リング進捗カードリスト
新しいタブで開く

09 · リング進捗カードリスト — 計測 累積面積 0.797% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

r=14animation-delayclick

どこを変えるか

行ごとに目標が違うのでキーフレームも行ごとに必要ですが、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

css 円 グラフや 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 がいちばん短い説明です。

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

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