読み込みバー コンポーネントの作成

<progress> 要素を使用して、色適応型でアクセスしやすい読み込みバーを構築する方法の基礎的な概要。

この投稿では、<progress> 要素を使用して、色に適応し、ユーザー補助に対応した読み込みバーを構築する方法について説明します。デモを試して、ソースを表示してください。

Chrome でデモされたライトモードとダークモード、不確定、増加、完了。

動画をご希望の場合は、この投稿の YouTube 版をご覧ください。

概要

<progress> 要素は、完了に関する視覚的および音声的なフィードバックをユーザーに提供します。この視覚的なフィードバックは、フォームの進行状況、ダウンロードまたはアップロード情報の表示、進行状況の量は不明だが作業がまだアクティブであることを示すなどのシナリオで役立ちます。

この GUI チャレンジでは、既存の HTML <progress> 要素を使用して、ユーザー補助の労力を軽減しました。色とレイアウトは、組み込み要素のカスタマイズの限界を押し広げ、コンポーネントをモダナイズして、デザイン システムに適合するようにしています。

各ブラウザのライトモードとダークモードのタブ。上から順に、Safari、Firefox、Chrome のアダプティブ アイコンの概要が表示されています。
Firefox、Safari、iOS Safari、Chrome、Android Chrome でライトモードとダークモードで表示されたデモ。

マークアップ

<progress> 要素を <label> でラップすることを選択したため、明示的な関係属性をスキップして暗黙的な関係を優先できます。また、読み込み状態の影響を受ける親要素にラベルを付けて、スクリーン リーダー技術がその情報をユーザーに伝えることができるようにしました。

<progress></progress>

value がない場合、要素の進行状況は不確定です。max 属性のデフォルト値は 1 であるため、進行状況は 0 ~ 1 の範囲になります。たとえば、max を 100 に設定すると、範囲は 0 ~ 100 に設定されます。ここでは 0 ~ 1 の範囲に収まるように、進捗値を 0.5(50%)に変換することにしました。

ラベルでラップされた進行状況

暗黙的な関係では、次のように progress 要素が label でラップされます。

<label>Loading progress<progress></progress></label>

このデモでは、スクリーン リーダーのみのラベルを含めるように選択しました。これを行うには、ラベルテキストを <span> でラップし、画面外に表示されるようにスタイルを適用します。

<label>
  <span class="sr-only">Loading progress</span>
  <progress></progress>
</label>

WebAIM の次の CSS を使用します。

.sr-only {
  clip: rect(1px, 1px, 1px, 1px);
  clip-path: inset(50%);
  height: 1px;
  width: 1px;
  margin: -1px;
  overflow: hidden;
  padding: 0;
  position: absolute;
}

devtools のスクリーンショット。画面準備完了のみの要素が表示されています。

読み込みの進行状況の影響を受ける領域

視覚に障がいがない場合は、進行状況インジケーターを関連する要素やページ領域に関連付けることは簡単ですが、視覚障がいのあるユーザーにとっては、それほど明確ではありません。これを改善するには、読み込みが完了したときに変更される最上位の要素に aria-busy 属性を割り当てます。さらに、aria-describedby を使用して、進行状況と読み込みゾーンの関係を示します。

<main id="loading-zone" aria-busy="true">
  …
  <progress aria-describedby="loading-zone"></progress>
</main>

JavaScript から、タスクの開始時に aria-busy を true に切り替え、完了したら false に切り替えます。

ARIA 属性の追加

<progress> 要素の暗黙的なロールは progressbar ですが、この暗黙的なロールがないブラウザに対して明示的に指定しています。また、indeterminate 属性を追加して、要素を明示的に不明な状態にしています。これは、要素に value が設定されていないことを確認するよりも明確です。

<label>
  Loading 
  <progress 
    indeterminate 
    role="progressbar" 
    aria-describedby="loading-zone"
    tabindex="-1"
  >unknown</progress>
</label>

tabindex="-1" を使用して、JavaScript から進行状況要素にフォーカスできるようにします。これは、スクリーン リーダー テクノロジーにとって重要です。進捗状況の変化に応じて進捗状況にフォーカスを合わせることで、更新された進捗状況がどこまで到達したかをユーザーに通知できるからです。

スタイル

progress 要素のスタイル設定は少し複雑です。組み込みの HTML 要素には、選択が難しい特別な非表示部分があり、多くの場合、設定できるプロパティのセットは限られています。

レイアウト

レイアウト スタイルは、進行状況要素のサイズとラベルの位置をある程度柔軟に設定できるようにすることを目的としています。特別な完了状態が追加されます。これは、必須ではありませんが、視覚的な手がかりとして役立ちます。

レイアウト: <progress>

進行状況要素の幅はそのままにして、デザインに必要なスペースに合わせて縮小、拡大できるようにします。組み込みスタイルは、appearance と border を none に設定することで削除されます。これは、各ブラウザに要素独自のスタイルがあるため、ブラウザ間で要素を正規化できるようにするためです。

progress {
  --_track-size: min(10px, 1ex);
  --_radius: 1e3px;

  /*  reset  */
  appearance: none;
  border: none;

  position: relative;
  height: var(--_track-size);
  border-radius: var(--_radius);
  overflow: hidden;
}

_radius の 1e3px の値は、科学的記数法を使用して大きな数値を表すため、border-radius は常に丸められます。これは 1000px と同じです。この方法が好まれるのは、設定してしまえば忘れてもよいほど大きな値を使用することが目的であるためです(また、1000px よりも短く記述できます)。必要に応じてさらに大きくすることも簡単です。3 を 4 に変更するだけで、1e4px は 10000px と同等になります。

overflow: hidden が使用されており、議論の余地があるスタイルとなっています。これにより、border-radius 値をトラックに渡す必要がない、トラックの塗りつぶし要素など、いくつかのことが簡単になりました。しかし、進行状況の子要素が要素の外に存在できないことも意味していました。このカスタム進行状況要素の別のイテレーションは、overflow: hidden なしで行うことができ、アニメーションやより良い完了状態の機会を開くことができます。

処理が完了しました

CSS セレクタは、最大値と値を比較することで、この難しい処理を行います。値が一致すると、進行状況は完了します。完了すると、疑似要素が生成され、progress 要素の末尾に追加されます。これにより、完了を示す視覚的な手がかりが追加されます。

progress:not([max])[value="1"]::before,
progress[max="100"][value="100"]::before {
  content: "✓";
  
  position: absolute;
  inset-block: 0;
  inset-inline: auto 0;
  display: flex;
  align-items: center;
  padding-inline-end: max(calc(var(--_track-size) / 4), 3px);

  color: white;
  font-size: calc(var(--_track-size) / 1.25);
}

読み込みバーが 100% になり、最後にチェックマークが表示されているスクリーンショット。

色

ブラウザは、進行状況要素に独自の色を適用し、1 つの CSS プロパティだけでライトモードとダークモードに適応します。これは、ブラウザ固有の特別なセレクタを使用して構築できます。

ライトモードとダークモードのブラウザ スタイル

サイトでダークモードとライトモードに適応する <progress> 要素を有効にするには、color-scheme のみが必要です。

progress {
  color-scheme: light dark;
}

単一プロパティの進行状況の塗りつぶしの色

<progress> 要素に色合いを付けるには、accent-color を使用します。

progress {
  accent-color: rebeccapurple;
}

トラックの背景色が、accent-color に応じて明るい色から暗い色に変化します。ブラウザが適切なコントラストを確保しています。

完全にカスタムのライトカラーとダークカラー

<progress> 要素に 2 つのカスタム プロパティを設定します。1 つはトラックの色、もう 1 つはトラックの進行状況の色です。prefers-color-scheme メディアクエリ内で、トラックとトラックの進行状況に新しい色値を指定します。

progress {
  --_track: hsl(228 100% 90%);
  --_progress: hsl(228 100% 50%);
}

@media (prefers-color-scheme: dark) {
  progress {
    --_track: hsl(228 20% 30%);
    --_progress: hsl(228 100% 75%);
  }
}

フォーカス スタイル

先ほど、プログラムでフォーカスできるように、要素に負のタブ インデックスを指定しました。:focus-visible を使用してフォーカスをカスタマイズし、よりスマートなフォーカス リング スタイルを有効にします。これにより、マウスクリックとフォーカスではフォーカスリングが表示されませんが、キーボード クリックでは表示されます。YouTube 動画では、この点について詳しく説明しています。

progress:focus-visible {
  outline-color: var(--_progress);
  outline-offset: 5px;
}

フォーカス リングで囲まれた読み込みバーのスクリーンショット。色がすべて一致している。

ブラウザ間でのカスタム スタイル

各ブラウザが公開する <progress> 要素の部分を選択して、スタイルをカスタマイズします。progress 要素は単一のタグですが、CSS 疑似セレクタを介して公開されるいくつかの子要素で構成されています。この設定を有効にすると、Chrome DevTools にこれらの要素が表示されます。

  1. ページを右クリックし、[要素を検証] を選択して DevTools を表示します。
  2. DevTools ウィンドウの右上にある設定アイコン(歯車)をクリックします。
  3. [Elements] の見出しの下にある [Show user agent shadow DOM] チェックボックスをオンにします。

DevTools でユーザー エージェント シャドウ DOM の公開を有効にする場所のスクリーンショット。

Safari と Chromium のスタイル

Safari や Chromium などの WebKit ベースのブラウザでは、::-webkit-progress-bar と ::-webkit-progress-value が公開されており、CSS のサブセットを使用できます。ここでは、先ほど作成したカスタム プロパティを使用して background-color を設定します。このプロパティは、ライトモードとダークモードに適応します。

/*  Safari/Chromium  */
progress[value]::-webkit-progress-bar {
  background-color: var(--_track);
}

progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
}

進行状況要素の内部要素を示すスクリーンショット。

Firefox のスタイル

Firefox では、<progress> 要素の ::-moz-progress-bar 疑似セレクタのみが公開されます。つまり、トラックに直接色を付けることはできません。

/*  Firefox  */
progress[value]::-moz-progress-bar {
  background-color: var(--_progress);
}

Firefox のスクリーンショットと、進行状況要素のパーツの場所。

デバッグ コーナーのスクリーンショット。Safari、iOS Safari、Firefox、Chrome、Android 版 Chrome のすべてで、読み込みバーが動作していることが示されています。

Firefox では accent-color からトラックの色が設定されていますが、iOS Safari ではトラックがライトブルーになっています。ダークモードでも同様です。Firefox にはダークトラックがありますが、設定したカスタムカラーはなく、WebKit ベースのブラウザで動作します。

アニメーション

ブラウザに組み込まれた疑似セレクタを使用する場合、多くの場合、許可される CSS プロパティのセットは制限されます。

トラックが埋まっていくアニメーション

進行状況要素の inline-size に遷移を追加すると、Chromium では機能しますが、Safari では機能しません。Firefox も、::-moz-progress-bar で transition プロパティを使用していません。

/*  Chromium Only 😢  */
progress[value]::-webkit-progress-value {
  background-color: var(--_progress);
  transition: inline-size .25s ease-out;
}

:indeterminate 状態のアニメーション

ここでは、少し工夫してアニメーションを提供できるようにします。Chromium の疑似要素が作成され、3 つのブラウザすべてで前後にアニメーション化されるグラデーションが適用されます。

カスタム プロパティ

カスタム プロパティはさまざまな用途に役立ちますが、特に便利なのは、魔法のように見える CSS 値に名前を付けることです。以下は、かなり複雑な linear-gradient ですが、名前はわかりやすいものです。その目的とユースケースを明確に理解できる。

progress {
  --_indeterminate-track: linear-gradient(to right,
    var(--_track) 45%,
    var(--_progress) 0%,
    var(--_progress) 55%,
    var(--_track) 0%
  );
  --_indeterminate-track-size: 225% 100%;
  --_indeterminate-track-animation: progress-loading 2s infinite ease;
}

カスタム プロパティは、コードを DRY に保つのにも役立ちます。ここでも、これらのブラウザ固有のセレクタをグループ化することはできません。

キーフレーム

目標は、前後に移動する無限のアニメーションです。開始キーフレームと終了キーフレームは CSS で設定されます。必要なキーフレームは 1 つだけです。50% の中央のキーフレームで、開始位置に戻るアニメーションを何度も繰り返します。

@keyframes progress-loading {
  50% {
    background-position: left; 
  }
}

各ブラウザをターゲットに設定する

すべてのブラウザで、<progress> 要素自体に疑似要素を作成したり、進行状況バーをアニメーション化したりできるわけではありません。疑似要素よりもトラックのアニメーションをサポートするブラウザが多いため、疑似要素をベースとしてアップグレードし、バーのアニメーションに移行します。

Chromium 疑似要素

Chromium では、疑似要素 ::after を位置指定で使用して要素を覆うことができます。不確定のカスタム プロパティが使用され、前後のアニメーションが適切に機能します。

progress:indeterminate::after {
  content: "";
  inset: 0;
  position: absolute;
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Safari の進行状況バー

Safari の場合、カスタム プロパティとアニメーションが疑似要素の進行状況バーに適用されます。

progress:indeterminate::-webkit-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}
Firefox の進行状況バー

Firefox の場合、カスタム プロパティとアニメーションは、疑似要素の進行状況バーにも適用されます。

progress:indeterminate::-moz-progress-bar {
  background: var(--_indeterminate-track);
  background-size: var(--_indeterminate-track-size);
  background-position: right; 
  animation: var(--_indeterminate-track-animation);
}

JavaScript

JavaScript は <progress> 要素で重要な役割を果たします。この属性は、要素に送信される値を制御し、スクリーン リーダーがドキュメント内の十分な情報を取得できるようにします。

const state = {
  val: null
}

このデモには、進行状況を制御するためのボタンが用意されています。これらのボタンは state.val を更新してから、DOM を更新する関数を呼び出します。

document.querySelector('#complete').addEventListener('click', e => {
  state.val = 1
  setProgress()
})

setProgress()

この関数で UI / UX のオーケストレーションが行われます。まず、setProgress() 関数を作成します。state オブジェクト、progress 要素、<main> ゾーンにアクセスできるため、パラメータは必要ありません。

const setProgress = () => {
  
}

<main> ゾーンの読み込みステータスの設定

進行状況が完了しているかどうかに応じて、関連する <main> 要素の aria-busy 属性を更新する必要があります。

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)
}

積載量が不明な場合は属性をクリアする

値が不明または未設定の場合(この例では null)、value 属性と aria-valuenow 属性を削除します。これにより、<progress> が不確定になります。

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }
}

JavaScript の 10 進数計算の問題を修正する

進行状況のデフォルトの最大値である 1 をそのまま使用することにしたため、デモの増分関数と減分関数では 10 進数の計算を使用します。JavaScript やその他の言語は、必ずしもこれに優れているとは限りません。以下は、計算結果の余分な部分を切り捨てる roundDecimals() 関数です。

const roundDecimals = (val, places) =>
  +(Math.round(val + "e+" + places)  + "e-" + places)

値を丸めて、表示可能で読みやすくします。

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"
}

スクリーン リーダーとブラウザの状態の値を設定する

この値は、DOM の次の 3 つの場所で使用されます。

  1. <progress> 要素の value 属性。
  2. aria-valuenow 属性。
  3. <progress> の内部テキスト コンテンツ。
const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent
}

進捗状況にフォーカスを合わせる

値が更新されると、晴眼ユーザーには進行状況の変化が表示されますが、スクリーン リーダーのユーザーには変化が通知されません。<progress> 要素にフォーカスすると、ブラウザが更新を通知します。

const setProgress = () => {
  zone.setAttribute('aria-busy', state.val < 1)

  if (state.val === null) {
    progress.removeAttribute('aria-valuenow')
    progress.removeAttribute('value')
    progress.focus()
    return
  }

  const val = roundDecimals(state.val, 2)
  const valPercent = val * 100 + "%"

  progress.value = val
  progress.setAttribute('aria-valuenow', valPercent)
  progress.innerText = valPercent

  progress.focus()
}

Mac OS Voice Over アプリが読み込みバーの進行状況をユーザーに読み上げているスクリーンショット。

まとめ

私の方法がわかったところで、あなたならどうしますか?🙂

もう一度チャンスがあれば、変更したい点がいくつかあります。現在のコンポーネントをクリーンアップする余地があり、<progress> 要素の疑似クラス スタイルの制限なしでコンポーネントを構築する余地があると思います。ぜひご検討ください。

アプローチを多様化し、ウェブで構築するすべての方法を学びましょう。

デモを作成して、ツイートでリンクをお知らせください。以下のコミュニティ リミックス セクションに追加します。

コミュニティ リミックス