CSSスケルトンスクリーンの実装方法
box-decoration-break
UI
スケルトンスクリーン
ローディング
コンテンツを読み込む際に、
読み込み待ちを表示するUIでもあるスケルトンスクリーンの実装について解説をしていきたいと思います。
実装方法はあくまで1例としてなので、これが正しいとは言いません。
さらに良い方法、ベストプラクティスというのがあれば教えていただきたいです。
スケルトンスクリーンとは
スケルトンスクリーンとはと言うことへのわかりやすい回答が、
上記になるのではないかと思います。
実際どういったものなのかを実装コードを一緒に表示します。
.ex-skelton {
width: 30.0rem;
padding: .5rem;
font-size: 1.6rem;
border: 1px solid currentColor;
}
.ex-skelton * {
overflow: hidden;
position: relative;
color: transparent;
}
.ex-skelton ._thumb {
aspect-ratio: 4 / 3;
background-color: #eee;
}
.ex-skelton ._tags {
display: flex;
flex-wrap: wrap;
margin-top: .5rem;
}
.ex-skelton ._tag {
margin-right: .5em;
padding: .2em .8em;
font-size: .8em;
background-color: #eee;
border-radius: 1.0em;
}
.ex-skelton ._txt {
margin-top: .5em;
background-color: #eee;
border-radius: 1.0em;
}
.ex-skelton ._date {
width: fit-content;
margin-top: .5em;
font-size: .8em;
background-color: #eee;
border-radius: 1.0em;
}
.ex-skelton ._date::before,
.ex-skelton ._txt::before,
.ex-skelton ._tag::before,
.ex-skelton ._thumb::before {
position: absolute;
top: 0;
left: 0;
z-index: 2;
content: '';
display: block;
width: 100%;
height: 100%;
background-image: linear-gradient(90deg, #ffffff00, #ffffff88, #ffffff00);
animation: skelton_flash 1.5s linear infinite;
will-change: animation;
}
@keyframes skelton_flash {
0% {
transform: translateX(-100%)
}
100% {
transform: translateX(100%)
}
}
「スケルトンスクリーン 実装」で調べると大体上の実装例が出てくる感じがしています。
各要素で overflow を効かせて、
擬似要素の before を animation で往復させる処理を行なって、
光の演出をするといった具合です。
スケルトンスクリーンのメリット・デメリット
メリット
メリットとしては表示される項目がなんなのかが一目でわかり、
パフォーマンス的にもレイアウトシフトが起こりにくいといった点だと思います。
デメリット
デメリットとしてはただのローディング画面よりは実装が面倒といった点になるぐらいでしょうか。
であれば、積極的にスケルトンスクリーンの実装をしていきたいところではあります。
みなさんはどうでしょうか。
よくある実装例の問題点
上に表示した実装例の問題点としては、
テキストの複数行を表現できないところでしょうか。
サムネイル・タグ・日付に関してはそのままの実装でも問題ないと思いますが、
テキストとが複数行になった場合は、大きなブロックになってしまうところに、不格好さが出てきてしまっていると思います。
右下に表示したものはテキスト部分を2行にして表示したために、
2行分のエリアを一つのエリアとしてスケルトン表示を行なってしまっています。
これに対する解決策
この2行問題を解決するための方法が、box-decoration-breakになります。
注:ブラウザにあったベンダープリフィックスが必要になる場合があります。
新たな実装方法
.ex-skelton-new {
width: 30.0rem;
padding: .5rem;
font-size: 1.6rem;
border: 1px solid currentColor;
}
.ex-skelton-new[aria-hidden="true"] span {
display: none;
}
.ex-skelton-new ._thumb {
aspect-ratio: 4 / 3;
background-image: linear-gradient(-45deg, #eee 25%, #f6f6f6 50%, #eee 75%);
background-size: 200% 100%;
background-repeat: repeat;
animation: skeltonTitle 20s linear infinite;
}
.ex-skelton-new ._tags {
display: flex;
flex-wrap: wrap;
margin-top: .5rem;
}
.ex-skelton-new ._tags-item {
margin-right: .5em;
font-size: .8em;
border-radius: 1.0em;
}
.ex-skelton-new ._tag {
padding: .3rem .5rem;
}
.ex-skelton-new ._txt {
margin-top: .5em;
border-radius: 1.0em;
}
.ex-skelton-new ._date {
width: fit-content;
margin-top: .5em;
font-size: .8em;
border-radius: 1.0em;
}
.ex-skelton-new ._tags-item::before {
content: 'dummy';
padding: .3rem .5rem;
}
.ex-skelton-new ._txt::before {
content: 'dummydummydummydummydummydummydummydummydummy';
word-break: break-all;
}
.ex-skelton-new ._date::before {
content: 'dummydummydumm';
}
.ex-skelton-new ._date::before,
.ex-skelton-new ._txt::before,
.ex-skelton-new ._tags-item::before {
display: inline;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
color: transparent;
line-height: 1.5;
background-image: linear-gradient(-45deg, #eee 25%, #f6f6f6 50%, #eee 75%);
background-size: 20rem 3rem;
background-repeat: repeat;
border-radius: 5rem;
animation: skeltonTitle 20s linear infinite;
}
@keyframes skeltonTitle {
0% {
background-position: 0 0;
}
100% {
background-position: 400rem 0;
}
}
若干コード量が多くなってしまいましたが、
元々の実装例をもとにテキスト部分のスケルトンスクリーン表現が2行で表現されました。
box-decoration-break
このスタイルプロパティは、下記のどちらかを値として設定します。
設定することで、テキストノードに対して、border 、border-radius 、background、box-shadowなどを適用させることができる様になります。
注:このスタイルプロパティを設定する要素は、inline要素であることが前提です。それ以外の時は効果を発揮しません。
- slice
- clone
slice を設定した時は、テキストノード全体を一つとして捉え、折り返された端を次の行へと続く形で分割されます。
設定例 )
Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptate eligendi magnam sequi doloremque cum debitis. Voluptatum odio, optio hic facilis obcaecati blanditiis cumque corrupti voluptate dolorum repudiandae laboriosam, ad voluptates?
clone を設定した時は、テキストノードを行毎に一つの要素として捉え、行にスタイルを与えることができます。
設定例 )
Lorem ipsum dolor sit amet consectetur adipisicing elit. Voluptate eligendi magnam sequi doloremque cum debitis. Voluptatum odio, optio hic facilis obcaecati blanditiis cumque corrupti voluptate dolorum repudiandae laboriosam, ad voluptates?
Javascriptを絡めた実装方法
スケルトンスクリーンはこのままでは意味のないもので、
Javascriptによってデータを取得し、データを流し込み、スケルトンスクリーンを解除することで、
本来の挙動になります。
なので、
Javascriptで操作しやすいように、アニメーションを起こしている箇所をクラスの付け外しで切り替えることができる様に、
cssを修正する必要があります。
さらに、aria-hidden="true"を要素に設定している(スケルトンスクリーンの時は読み上げ対象にしない)ので、
データの流し込みが終わった後に、この属性と属性値を削除する必要があります。
実装例
下記はAPIからデータを取得して要素に流し込む実装の一例です。
データの読み込みに行くタイミングやデータを取得完了するタイミング、
表示するデータの個数、表示するための要素の構造は各環境によって違うので、
その辺りは個別に実装をしてください。
fetch('/api/xxx/')
.then(res => res.json())
.then(data => {
const { thumbImgPath, tags, txt, date } = data;
const card = document.querySelector('.demo-card')
const thumbFrame = card.querySelector('._thumb')
const txtFrame = card.querySelector('._txt')
const dateFrame = card.querySelector('._date')
thumbFrame.style.backgroundImage = `url(${thumbImgPath})`
txtFrame.textContent = txt
dateFrame.textContent = date
const tagsFrame = card.querySelector('._tags')
const tagItemString = tagsFrame.firstElementChild.outerHTML
tagsFrame.innerHTML = tags.map(tag => {
return tagItemString.replace('XXX', tag)
}).join()
card.classList.remove('_skelton')
card.removeAttribute('aria-hidden')
})
.demo-card {
width: 30.0rem;
padding: .5rem;
font-size: 1.6rem;
border: 1px solid currentColor;
}
.demo-card._skelton[aria-hidden="true"] span {
display: none;
}
.demo-card ._thumb {
aspect-ratio: 4 / 3;
background-size: cover;
background-repeat: no-repeat;
}
.demo-card._skelton ._thumb {
aspect-ratio: 4 / 3;
background-image: linear-gradient(-45deg, #eee 25%, #f6f6f6 50%, #eee 75%);
background-size: 200% 100%;
background-repeat: repeat;
animation: skeltonTitle 20s linear infinite;
}
.demo-card ._tags {
display: flex;
flex-wrap: wrap;
margin-top: .5rem;
}
.demo-card ._tags-item {
margin-right: .5em;
font-size: .8em;
border-radius: 1.0em;
}
.demo-card ._tag {
padding: .3rem .5rem;
}
.demo-card ._txt {
margin-top: .5em;
border-radius: 1.0em;
}
.demo-card ._date {
width: fit-content;
margin-top: .5em;
font-size: .8em;
border-radius: 1.0em;
}
.demo-card._skelton ._tags-item::before {
content: 'dummy';
padding: .3rem .5rem;
}
.demo-card._skelton ._txt::before {
content: 'dummydummydummydummydummydummydummydummydummy';
word-break: break-all;
}
.demo-card._skelton ._date::before {
content: 'dummydummydumm';
}
.demo-card._skelton ._date::before,
.demo-card._skelton ._txt::before,
.demo-card._skelton ._tags-item::before {
display: inline;
-webkit-box-decoration-break: clone;
box-decoration-break: clone;
color: transparent;
line-height: 1.5;
background-image: linear-gradient(-45deg, #eee 25%, #f6f6f6 50%, #eee 75%);
background-size: 20rem 3rem;
background-repeat: repeat;
border-radius: 5rem;
animation: skeltonTitle 20s linear infinite;
}
@keyframes skeltonTitle {
0% {
background-position: 0 0;
}
100% {
background-position: 400rem 0;
}
}
必要最低限の動作を行うデモを実装をしました。
上記の実装例とは若干違いますが、表示切り替えに関する部分は同じ動作になっています。
ボタンを押すことによって、
サムネイルのURL、紐づけられているタグ、ページタイトル、投稿日を今表示されているページから取得しています。
データの取得後は、
実装例に書かれているように、セレクタである_skeltonを外し、
読み上げをしない様にしている属性のaria-hidden="true"を削除しています。
まとめ
スケルトンスクリーンの実装はそれほど難しくはないと思いますが、
最優先で実装されるべきではないところなので、
どうしても、ただのローディングを表示する形を取ってしまうことがあると思います。
しかしながら、
パフォーマンスの観点からも有用なUIなので、実装する機会があるのであれば積極的に実装をできたら良いと思います。
Jamstack(SSG: Static Site Generation)による実装であれば、
この様な実装自体がされないので、さらにパフォーマンスは高いかと思います。