React Content Loaderでローディングプレースホルダーを実装する
あけましておめでとうございます。年末の空いた時間を利用してこのWebサイトでローディングプレースホルダーを表示するようにしたので、その紹介をしたいと思います。
ローディングプレースホルダーというのは、データの取得などでユーザーに待ってもらう間、実際のコンテンツに似た形状のローディング表示をするUIのことです。
古くから使われている丸い形のローディング表示はスピナーと呼ばれていますが、スピナーと違って実際のコンテンツに似ているので視線を自然にコンテンツが表示される位置に誘導でき、また仮の表示でコンテンツの空白を埋められるので、徐々にコンテンツが読み込まれていく時に起こるガタガタ感を軽減することができます。
React Content Loader
📦 react-content-loader
はSVGにクリップパスを当てることで複数の矩形要素を横断したアニメーションでローディングプレースホルダーを実装できるライブラリです。
ちなみにVue版の egoist/vue-content-loader やAngular版の ngneat/content-loader などもあります。使い方は同じです。
<ContentLoader>
というReactコンポーネントがexportされているので、この中に <rect>
や <circle>
を入れていきます。ローディング完了後に表示される実際のコンテンツからサイズやポジションを計算して <rect>
を作ります。たとえばこのWebサイトのブログ記事ページは読み込み中に次のような表示になっています。端末をオフラインにして言語を切り替えようとすると試せます。
2020/01/06 追記
現在はService Workerによるオフライン対応が施されているので、再現にはキャッシュを全て消した上で端末をオフラインにする必要があります。
一般的にテキストには文字のサイズ (font-size
) と行間 (line-height
) があります。これらを意識して <rect>
を作っていくとかなりそれっぽくなります。
とはいえ <rect>
に line-height
はありませんので、 font-size
と加味して x
と height
で表示位置を設定します。考え方は次の画像のようになります。
これをReactのJSXで表現すると次のようになります。
/* 記事の本文のCSS (実際とは異なりますが参考として) */
p {
font-size: 16px;
line-height: 1.75;
}
// line-heightの1.75をpx換算すると28px
// テキストの上下に (28 - 16) / 2 = 6pxの空白があることになる
<ContentLoader>
{/* テキストの上に空白があるので一行目は y=6px になる */}
<rect x="0" y="6px" width="100%" height="16px" />
{/* 2行目以降は 前の行のy + 16px + 12px */}
<rect x="0" y="34px" width="100%" height="16px" />
<rect x="0" y="62px" width="40%" height="16px" />
</ContentLoader>
ダークモードに対応する
塗りつぶしの色に透明度を持たせればダークモードでもそれなりの見た目になりますが、背景がピュアな黒でない時などは違和感があるかもしれません。実際このWebサイトの背景色は #11181f
で、うっすら青色が強いです。こういう場合はSVGの塗りつぶしの定義となる <linearGradient>
要素内にある <stop>
要素にメディアクエリで別々の色を当てるようにするとよいです。
/* 実際にはsvg要素の代わりにclassを指定してください */
svg > defs > linearGradient > stop:nth-of-type(2n) {
stop-color: #e0e4e9;
}
svg > defs > linearGradient > stop:nth-of-type(2n + 1) {
stop-color: #eff2f4;
}
@media (prefers-color-scheme: dark) {
svg > defs > linearGradient > stop:nth-of-type(2n) {
stop-color: #1e2730;
}
svg > defs > linearGradient > stop:nth-of-type(2n + 1) {
stop-color: #2d3641;
}
}
このWebサイトはStyled Componentsを利用しているので各要素の参照は &
というシンボルを介しています。ソースコードはこちらです。
レスポンシブにする
SVGはHTMLに埋め込めるベクター画像のようなものなのでアスペクト比を維持してしまうと、幅に応じて高さが自動的に変わってしまい不自然です。次のことに気をつけましょう。
- 塗りつぶしの定義要素 (
<rect>
や<circle>
など) のwidth
やheight
をパーセンテージで定義する viewBox
やpreserveAspectRatio
をデフォルト値のまま変えないwidth
とheight
を計算して定義しておく- 文字サイズ、余白などでの合計
height
を算出して固定しておく (サンプル) 。
- 文字サイズ、余白などでの合計
既知の問題: <base>
との併用
React Content LoaderはSVG要素のクリップパス (塗りつぶしをどう切り抜くか) やグラデーションの定義に相対パスでの url()
を利用しているので、 <base>
要素を <head>
内に定義している場合は黒い塗りつぶしが表示されてしまいうまく動きません。クリップパスやグラデーションの定義にURLでたどり着けないためです。
Safariでのみこの問題が発生するのでSafariでのバグのように感じてしまいますが、SVG WGの見解によるとそれが正しい挙動なようです。 <ContentLoader>
Reactコンポーネントに baseUrl
というPropsがあるのでそれを利用して url()
にプレフィックスを付与するか、 <base>
要素を使わないようにするなどして回避しましょう。
Webpackを利用している場合は webpack.config.js
で output.publicPath
を設定するなどすれば大抵の場合は <base>
要素が必要なくなります。
まとめ
- ローディング中のうちにユーザーの視線を誘導するためにローディングプレースホルダーを使いましょう
- 視線を正しく誘導するためにローディングプレースホルダーを実際のコンテンツに似せましょう
そんなわけで、今年もよろしくお願いします!