jackmiwamiwa devblog

フロントエンドをメインに行ったことをまとめていくサイトになります。

スケルトンスクリーンをちゃんと理解する

ケルトンスクリーンを実装する時にいろいろと調べたので、その時のメモになります。

ケルトンスクリーンとは

引用元1

ws-design.net

簡単に言うと骨組みローディング画面です。
ローディング画面を必要とするまでもないくらい短い読み込みが発生する場合が適しています。
ケルトンスクリーン以外にも以下のものがある
ブランクスクリーン(画像左)
ローディングスピナー(画面中央)
ケルトンスクリーン(画面右)

それぞれの内容については引用元1参照

引用元2

kuroeveryday.blogspot.com

ケルトンスクリーンは、画像やCSSJavaScriptを読み込んでいる間にワイヤーフレームのようなボックスを表示し、UXを向上させるために使われる。ユーザにとってはプログレスバーやスピナーと違いどんなページが表示されるか予想できるため、ロード時間が長くても心理的に短く感じられる。
ただ、スケルトンスクリーンだけだと動きがないため、ちゃんと処理されているか不安になる。そこでシマー効果(キラキラさせるエフェクト)をつけることで、スピナーとしての役割も果たすことができる。
完成予想図は下図のとおり。

今回実装したものについて

3秒間スケルトンスクリーンを表示して、その後に要素を表示するイメージです。

See the Pen skeleton-screen by miwa_shuntaro (@miwashutaro0611) on CodePen.

参考にしたもの

qiita.com

egghead.io

コード

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;
}

JavaScript

/**
 * スケルトンスクリーンの処理
 * @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. 大枠のマークアップ作成

ケルトンの親要素に対しては以下を指定

  • skeleton : スケルトンの大枠のスタイルの適応のため(今回は不使用)
  • js-skeleton : スケルトンのjs発火用のclass

また、UI自体の追加をしたい場合は以下のhogehogeを作成し、skeletonに付与するのではなく、hogehogeに対してスタイルを付与する。

<div class="skeleton js-skeleton hogehoge">なにか</div>

理由:skeletonにUI自体のスタイルを付与してしまうと以下のような複数のケースが発生した際に対応できなくなる可能性があるため

  • 縦長のカード
  • 横長のカード
  • 特殊なUI

1-2. 大枠のa11y対応

対応したことについて

egghead.io

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

developer.mozilla.org

tabindexの指定を行うことで、フォーカルがスケルトンスクリーンに当たるように設定します。

  • role="progressbar"

developer.mozilla.org

長い時間がかかったり、いくつかの手順で構成されるタスクの進捗状況を表示するために使用。 プログレスバーはユーザーの要求を受けて、アプリケーションが要求された操作を完了に向かって進捗していることを示すため、設定します。

www.osaka-kyoiku.ac.jp

現在更新中かどうかを判定させるために使用。 既定値は、aria-busyがfalseで、更新中のものの場合のみtrueを設定します。

developer.mozilla.org

developer.mozilla.org

先ほどrole="progressbar"の設定を行ったため、範囲ウィジェットに許容される最小値・最大値を定義するために使用。

developer.mozilla.org

今回のスケルトンスクリーンのものについて0% - 100%などの状態を表すことができる場合、以下のaria-valuenowを使用した方がよいが、今回の場合は状態を表すことができないため、aria-valuetextを使用して代替テキストの定義を行う

developer.mozilla.org

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. ページが読み込まれたタイミングでスケルトン要素・既存の表示させたい要素の取得

  • js-skeleton : スケルトンスクリーンの要素(複数指定を想定)
  • js-pageContent : スケルトンスクリーン表示後に表示させたい要素(複数指定を想定)
window.addEventListener('load', () => {
  const skeletonElems = document.querySelectorAll(".js-skeleton")
  const pageContentElems = document.querySelectorAll(".js-pageContent")
}, false)

3-2. スケルトンの要素を削除

読み込みが終わったタイミングでスケルトンスクリーンの要素は不要のため、削除する display: noneでも良いが、不要なelementは残しておきたくないので、remove()を使用

developer.mozilla.org

今回実装した処理

const deleteSkeleton = (skeletonElems) => {
  if(skeletonElems.length === 0) return // 要素がそもそもなければ早期return実行
  skeletonElems.forEach((elem) => {
    elem.remove()
  })
}

3-3. 既存の要素を表示

表示させたい要素に対してhidden属性を付与しているため、要素の読み込みが完了したタイミングでその要素を削除する

<div class="js-pageContent" hidden>何かのコンテンツ</div>

developer.mozilla.org

今回実装した処理

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つの候補として置いておいてもらえると嬉しいです。

ws-design.net

終結果のものをあたらめて置いておきます。

See the Pen skeleton-screen by miwa_shuntaro (@miwashutaro0611) on CodePen.