スケルトンスクリーンをちゃんと理解する
スケルトンスクリーンを実装する時にいろいろと調べたので、その時のメモになります。
スケルトンスクリーンとは
引用元1
簡単に言うと骨組みローディング画面です。
ローディング画面を必要とするまでもないくらい短い読み込みが発生する場合が適しています。
スケルトンスクリーン以外にも以下のものがある
ブランクスクリーン(画像左)
ローディングスピナー(画面中央)
スケルトンスクリーン(画面右)
それぞれの内容については引用元1参照
引用元2
スケルトンスクリーンは、画像やCSS、JavaScriptを読み込んでいる間にワイヤーフレームのようなボックスを表示し、UXを向上させるために使われる。ユーザにとってはプログレスバーやスピナーと違いどんなページが表示されるか予想できるため、ロード時間が長くても心理的に短く感じられる。
ただ、スケルトンスクリーンだけだと動きがないため、ちゃんと処理されているか不安になる。そこでシマー効果(キラキラさせるエフェクト)をつけることで、スピナーとしての役割も果たすことができる。
完成予想図は下図のとおり。
今回実装したものについて
3秒間スケルトンスクリーンを表示して、その後に要素を表示するイメージです。
See the Pen skeleton-screen by miwa_shuntaro (@miwashutaro0611) on CodePen.
参考にしたもの
コード
html
<!-- スケルトンスクリーンのマークアップ @see https://egghead.io/lessons/aria-use-wai-aria-attributes-to-improve-web-accessibility-of-your-skeleton-loader --> <!-- なにかのコンテンツ --> <div class="js-pageContent" hidden>何かのコンテンツ</div> <!-- スケルトンスクリーン --> <div class="skeleton js-skeleton" tabindex="0" role="progressbar" aria-busy="true" aria-valuemin="0" aria-valuemax="100" aria-valuetext="Please wait..." > <div class="skeleton__title"></div> <div class="skeleton__content"></div> <div class="skeleton__image"></div> </div>
scss
/** * スケルトンスクリーンのスタイル * @see https://qiita.com/kanachimu/items/481c2459ef4ec298f47f */ $skeletonBaseColor: #e2e2e2; $skeletonMaskColor: rgba(255, 255, 255, 0.2); $skeletonTime: 1.2s; $skeletonEase: linear; @keyframes skeleton-animation { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @mixin skeleton-mixin($width, $height, $radius: 0px) { position: relative; width: $width; height: $height; overflow: hidden; background-color: $skeletonBaseColor; @if $radius > 0px { border-radius: $radius; } &::before { position: absolute; top: 0; left: 0; z-index: 1; content: ""; display: block; height: 100%; width: 100%; background: linear-gradient( 90deg, transparent, $skeletonMaskColor, transparent ); animation: skeleton-animation $skeletonTime $skeletonEase infinite; } } .skeleton__title { @include skeleton-mixin(300px, 50px); } .skeleton__content { @include skeleton-mixin(300px, 200px, 8px); margin-top: 10px; } .skeleton__image { @include skeleton-mixin(50px, 50px, 9999px); margin-top: 10px; }
/** * スケルトンスクリーンの処理 * @see https://qiita.com/kanachimu/items/481c2459ef4ec298f47f */ // スケルトン自体の要素を削除する const deleteSkeleton = (skeletonElems) => { if(skeletonElems.length === 0) return skeletonElems.forEach((elem) => { elem.remove() }) } // スケルトン表示中に隠していたコンテンツを表示する const showContent = (pageContentElems) => { if(pageContentElems.length === 0) return pageContentElems.forEach((elem) => { elem.hidden = false }) } // 読み込みが完了した時に行う処理 const loadContent = (skeletonElems, pageContentElems) => { deleteSkeleton(skeletonElems) showContent(pageContentElems) } window.addEventListener('load', () => { const skeletonElems = document.querySelectorAll(".js-skeleton") const pageContentElems = document.querySelectorAll(".js-pageContent") // NOTE: 今回はロード後3秒経過したタイミングだが、何かのAPIが読み込まれた時などの時には以下の関数のタイミングを変更する setTimeout(() => { loadContent(skeletonElems, pageContentElems) }, 3000) }, false)
実装した手順について
1. マークアップの作成
1-1. 大枠のマークアップ作成
スケルトンの親要素に対しては以下を指定
また、UI自体の追加をしたい場合は以下のhogehoge
を作成し、skeleton
に付与するのではなく、hogehoge
に対してスタイルを付与する。
<div class="skeleton js-skeleton hogehoge">なにか</div>
理由:skeleton
にUI自体のスタイルを付与してしまうと以下のような複数のケースが発生した際に対応できなくなる可能性があるため
- 縦長のカード
- 横長のカード
- 特殊なUI
1-2. 大枠のa11y対応
対応したことについて
Instructor: [0:00] Now we can add aria attributes on the HTML of our skeleton, so that users with any type of visual disabilities can understand that specific state of the fragment or a page is current loading.
[0:15] Let's add some attributes. The first one is tabindex, we add this as zero so screen readers cannot have this focused. The second attribute is role, so it can say to screen readers that's a ProgressBar. The next attribute is aria-busy and we add this value as true, so that we can make sure that screen readers understand that it's a content that will be updated.
[0:46] Since it's a ProgressBar, we need to add margin parameters. One is aria-valuemin as and aria-valuemax as 100, because it's a ProgressBar. The next attribute is aria-valuetext. This is quite important, so we can pass the text that will be read by the screen readers. The screen we're using "Please wait."
[1:13] Now we have all the attributes required for the screen readers and other assistive technologies. Let's check the effects of our changes in Lighthouse. First, we need to run our local server, by run npx serve and the local folder. With that, we have the local URL that we can check in our browser.
[1:36] After that, we can open Lighthouse, make sure that we have Accessibility icon pressed, and run the audition. As soon as Lighthouse finished the audition, a new page will be opened with all the items that were checked. The items that need to be checked manually are more than these, the items that were passed in the audition.
[2:00] All the aria levels and the aria-hidden="true", for example, and role, and all the relevant information for the screen readers will be there as green items, which means they are good to go.
対応した属性について
- tabindex
tabindexの指定を行うことで、フォーカルがスケルトンスクリーンに当たるように設定します。
- role="progressbar"
長い時間がかかったり、いくつかの手順で構成されるタスクの進捗状況を表示するために使用。 プログレスバーはユーザーの要求を受けて、アプリケーションが要求された操作を完了に向かって進捗していることを示すため、設定します。
- aria-busy
現在更新中かどうかを判定させるために使用。
既定値は、aria-busyがfalse
で、更新中のものの場合のみtrue
を設定します。
先ほどrole="progressbar"
の設定を行ったため、範囲ウィジェットに許容される最小値・最大値を定義するために使用。
- aria-valuetext
今回のスケルトンスクリーンのものについて0% - 100%
などの状態を表すことができる場合、以下のaria-valuenow
を使用した方がよいが、今回の場合は状態を表すことができないため、aria-valuetext
を使用して代替テキストの定義を行う
1-3. 必要な箇所にスケルトンUIを作成していく
先ほど制作したskeleton
の要素の中にtitle
のエリアやimage
のUI部分を入れていく。
今回制作したものについては以下になります。
<!-- 一番上の要素 --> <div class="skeleton__title"></div> <!-- 真ん中の要素 --> <div class="skeleton__content"></div> <!-- 一番下の要素 --> <div class="skeleton__image"></div>
2. skeletonのスタイルのscssのmixinを作成
スケルトンスクリーンについて、場所は違っても処理については以下で共通のため、スケルトンスクリーン用のmixinを作成
- コンテンツの横幅・縦幅・角丸の値の指定(これは可変)
- 要素の外の幅より超えるものは非表示(擬似要素で横移動のスライドをさせるため、要素外の表示はさせないため)
- 擬似要素で白色のスピナー部分を作成 動きについては以下のように動きます。(検証のため、色を黒色にしています。)
See the Pen skeleton-screen by miwa_shuntaro (@miwashutaro0611) on CodePen.
$skeletonBaseColor: #e2e2e2; $skeletonMaskColor: rgba(255, 255, 255, 0.2); $skeletonTime: 1.2s; $skeletonEase: linear; @keyframes skeleton-animation { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } @mixin skeleton-mixin($width, $height, $radius: 0px) { position: relative; width: $width; height: $height; overflow: hidden; background-color: $skeletonBaseColor; @if $radius > 0px { border-radius: $radius; } &::before { position: absolute; top: 0; left: 0; z-index: 1; content: ""; display: block; height: 100%; width: 100%; background: linear-gradient( 90deg, transparent, $skeletonMaskColor, transparent ); animation: skeleton-animation $skeletonTime $skeletonEase infinite; } }
また、横移動のアニメーションについても以下のkeyframes
にて、指定します。
@keyframes skeleton-animation { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
mixinの指定については完了したため、呼び出す時には以下のイメージで呼び出しを行えば使用することができます。
.skeleton__title { // 横幅・縦幅のみの指定 @include skeleton-mixin(300px, 50px); } .skeleton__content { // 横幅・縦幅・角丸の値の指定 @include skeleton-mixin(300px, 200px, 8px); // 要素間の余白を空けたいときなどはmixin外で指定 margin-top: 10px; }
3. 任意のタイミング(今回は読み込み後に3秒後)でスケルトンスクリーンの削除・任意の要素の表示
3-1. ページが読み込まれたタイミングでスケルトン要素・既存の表示させたい要素の取得
window.addEventListener('load', () => { const skeletonElems = document.querySelectorAll(".js-skeleton") const pageContentElems = document.querySelectorAll(".js-pageContent") }, false)
3-2. スケルトンの要素を削除
読み込みが終わったタイミングでスケルトンスクリーンの要素は不要のため、削除する
display: none
でも良いが、不要なelementは残しておきたくないので、remove()
を使用
今回実装した処理
const deleteSkeleton = (skeletonElems) => { if(skeletonElems.length === 0) return // 要素がそもそもなければ早期return実行 skeletonElems.forEach((elem) => { elem.remove() }) }
3-3. 既存の要素を表示
表示させたい要素に対してhidden属性
を付与しているため、要素の読み込みが完了したタイミングでその要素を削除する
<div class="js-pageContent" hidden>何かのコンテンツ</div>
今回実装した処理
const showContent = (pageContentElems) => { if(pageContentElems.length === 0) return // 要素がそもそもなければ早期return実行 pageContentElems.forEach((elem) => { elem.hidden = false }) }
補足
hidden属性
が付与されている場合、[Attributes Style]
でdisplay: none
が指定されているため、非表示になるが、既存のclass名でdisplay: flex
などdisplayプロパティが指定されている場合は非表示にならない可能性があるため、念のため、当てておいた方がよいもののcssを全体に適応させておくと良い
元々当たっているもの
div[Attributes Style] { display: none; }
念のため、当てておいた方がよいもの
[hidden] { display: none !important; }
最後に
以下の記事でもスケルトンスクリーンのUXについて記載されていますが、どのケースでもスケルトンスクリーンが適切とは限らないため適切なシーンに応じて使用することで、ちゃんと効果を発揮できると思うので、ぜひローディングが長い場合の施策としてスケルトンスクリーンも1つの候補として置いておいてもらえると嬉しいです。
最終結果のものをあたらめて置いておきます。
See the Pen skeleton-screen by miwa_shuntaro (@miwashutaro0611) on CodePen.