進捗状況グラフ

進捗状況グラフ(円形の進行状況バー)は、タスクのステータスを視覚的にフィードバックします。線形の進行状況バーとは異なり、円形の形状はダッシュボード、カード コンポーネント、スペースが限られている場所に適しています。

このガイドでは、次の方法で進捗状況グラフを実装します。

  • ネイティブの <progress> 要素をセマンティック基盤として使用し、スクリーン リーダーやキーボード ユーザーがコンポーネントにすぐにアクセスできるようにします。
  • conic-gradient() と mask-image を使用してコンポーネントのスタイルを設定することで、SVG パス操作の複雑さを伴わずに、完全にレスポンシブでテーマ設定可能なリングを実現できます。
  • CSS カスタム プロパティと @property を活用して、GPU で高速化されたスムーズな進行状況バーの塗りつぶしを実現しています。

このアプローチは、ARIA ではなくセマンティックな <progress> 要素を使用し、既存のレイアウト、デザイン システム、タイポグラフィと簡単に統合できるため、SVG のみのソリューションよりも推奨されます。

不確定な読み込み状態の処理については、読み込みスピナーをご覧ください。

実装

1. マークアップ

ラッパーを使用して、ビジュアル リングとオプションの中央コンテンツの両方を保持します。<progress> 要素は、引き続きセマンティックな信頼できる情報源となります。ユーティリティ クラスを使用して、ネイティブの進行状況バーを視覚的に非表示にしつつ、アクセス可能な状態を維持します。

<div class="ring-wrapper">
  <progress value="75" max="100" aria-label="Task progress" class="progress-ring"></progress>
  <!-- Optional: Content to display in the center -->
  <div class="ring-content">
    75%
  </div>
</div>

2. スタイル

ネイティブ UI を非表示にする

<progress> 要素を進行状況リングとしてスタイル設定するには、まず、進行状況バーのデフォルトのブラウザ スタイルを非表示にします。

/* Hide native bars */
progress.loading-spinner:indeterminate::-webkit-progress-bar {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::-webkit-progress-value {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::-moz-progress-bar {
  display: none;
  background: none;
}
progress.loading-spinner:indeterminate::slider-fill {
  display: none;
  background: none;
}

コンテナとリング

ラッパーは、位置コンテキストを提供します。<progress> 要素は、視覚的なグラデーションを処理します。

.progress-ring-wrapper {
  position: relative;
  display: grid;
  place-items: center;
}

progress.progress-ring {
  --size: 150px;
  --thickness: 16px;
  --track-color: #f1f5f9;
  --fill-color: #3b82f6;
  --value: attr(value type(<number>));
  width: var(--size);
  height: var(--size);

  transition: --value 0.4s cubic-bezier(0.4, 0, 0.2, 1);
  border-radius: 50%;

  background: conic-gradient(
    var(--fill-color) calc(var(--value) * 1%),
    var(--track-color) 0
  );

  /* MANDATORY: Clip the background to the border-area. */
  background-clip: border-area;
  border: var(--thickness) solid transparent;
  background-origin: border-box;
}

.progress-ring-content {
  /* Positioned in the center of the wrapper */
  position: absolute;
}

radial-gradient を使用して、丸みを帯びたエンドキャップを作成することもできます。

@property でスムーズな遷移を可能にする

値が変更されたときに進捗状況グラフをスムーズにアニメーション化するには、--value を数値のカスタム プロパティとして登録します。

動きに敏感なユーザーは、値の切り替えに戸惑うことがあります。デフォルトで 0 秒(即時)の期間を設定し、設定のないユーザーにはより長い時間を設定することで、prefers-reduced-motion メディアクエリを尊重します。

@property --value {
  syntax: '<number>';
  inherits: true;
  initial-value: 0;
}

progress.progress-ring {
  transition: --value 0s ease-in-out;
  @media (prefers-reduced-motion: no-preference) {
    transition-duration: 0.4s;
  }
}

3. 進捗状況の更新

値が変更されるたびに、<progress> 要素の value 属性と .progress-ring-content のテキスト コンテンツ(パーセンテージを表示する場合)を更新します。

4. オプションの成功状態

CSS 属性セレクタを使用すると、タスクが完了したときにリングの外観を自動的に更新できます(色を緑に変更するなど)。

/* Change the fill color to green when the progress reaches 100% */
progress.progress-ring[value="100"] {
  --fill-color: #10b981;
}

代替戦略

<progress> 要素内にフォールバック値を追加しないでください。支援技術では使用されず、最新のブラウザではすべて無視されます。

アニメーションの代替

@property がサポートされていない場合、リングはスムーズに移行するのではなく、新しい値にすぐにジャンプします。ほとんどの場合、これは問題なく、機能が損なわれることはありません。

どうしても遷移が必要な場合は、@property のサポートを確認し、requestAnimationFrame() ループを使用して古いブラウザで --value を補間できます。

background-clip: border-area をまだサポートしていないブラウザの場合は、mask-image にフォールバックして <progress> 要素の中央をくり抜きます。

@supports not (background-clip: border-area) {
  mask-image: radial-gradient(
    transparent calc(50% - var(--thickness)),
    black calc(50% - var(--thickness) + 0.5px)
  );
  border: 0;
}

attr() CSS 関数をサポートしていないブラウザでは、MutationObserver を使用して value 属性を --value カスタム プロパティに自動的に同期します。

if (!CSS.supports("width: attr(value type(<number>))")) {
  const observer = new MutationObserver(() => {
    progress.style.setProperty("--value", progress.getAttribute("value"));
  });
  observer.observe(progress, {
    attributes: true,
    attributeFilter: ["value"],
  });
}