メインコンテンツまでスキップ

当ブログのレスポンシブコーディングについて

Development

当ブログのレスポンシブコーディング施策のまとめです。

メディアクエリよりもコンテナクエリを優先する

前回の記事でも触れたようにメディアクエリを一切使わずレスポンシブコーディングしました。 僕がメディアクエリを使用しなかった理由は以下の点が気になっていたからです。

  • 各コンポーネントの状態変化をウィンドウのサイズに依存させるのは都合が悪い。実装者はウィンドウのサイズとにらめっこしながらデザインを調整する必要があり、非常に面倒。
  • ある程度の的確な位置・間隔でブレイクポイントを用意するコーディングは効率的だが、全ての画面サイズで完璧な表示を実現するのが難しい。必ずどこかしらのサイズで見た目を妥協しないといけなくなってくる。
  • ウィンドウのサイズではなく各コンポーネントのサイズを基準にデザイン調整するなら、どのように配置されるかを細かく考える必要がなくなる。代わりに、それぞれのコンポーネントが含まれるコンテナ(親要素)のサイズに応じて、どのように見えるかだけを考えればよいので、開発がより単純で論理的になる。

といった事情からメディアクエリを捨ててコンポーネントのルートを基準としたコンテナクエリでレスポンシブ施策を行いました。

コンテナクエリ自体が真新しいこともあってなかなか実務で使用する機会が無く、今回の施策でガッツリ使用した感想としては、従来のメディアクエリを使った方法よりも圧倒的にデザイン調整が容易く、故に記述量も少なくなりました

しかし、コンテナクエリは基準となるコンテナ要素(containerプロパティを指定した要素)に使えないという仕様上、標準を定めるためだけの余分なdivを増やす必要があったりとHTMLの構造を不必要に複雑にする可能性があることは気がかりでした。また、今回のようにデザインと実装を両方担当できる案件ならともかく、分業の場合はデザイナーと開発者の間で細かいすり合わせが必要になる可能性があるように感じました。コンテナクエリは要素のサイズに基づいてスタイルが変更されるため、静的なデザインカンプを基準に作業を進めるのが難しくなり、デザインと実装の間でより細かな調整が必要になるかもしれません。

それでもコンテナクエリを使用するメリットはかなりのものでしたので、(対象範囲が許す限りは)実務でも積極的に利用したく思います。

font-sizeはclamp()関数で指定する

使用されている現場は多いとは思いますが、当ブログはfont-sizeclamp()関数を利用しています。

画面サイズに応じてフォントサイズが自動的に調整されるため、様々な画面サイズに対応するための複数のメディアクエリやフォントサイズの定義を減らすことができます。故に実装コストが減り、またコードの簡潔化に繋がります。

また、テキストのサイズが適切に保たれるため、ユーザビリティ向上に繋がるかもしれません。どこかの画面幅では大きすぎる、もしくは小さすぎるみたいな現象が起こりにくいのは大きな利点です。

clamp()の式の計算はSassの@functionやMin-Max-Value Interpolationといったオンラインツールで算出することが多いと思いますが、前者は当ブログではSassを使用していないので使用できず、後者はマジックナンバーを指定することに繋がり、コメントを逐一残しておかないと元の値が分かりにくくなってしまうデメリットがあります。また、わざわざツールを開いて計算を行うのは面倒です。

そういった事情から当ブログではCSS変数を使用してclamp()の計算を行い、またすべての要素で最小値、最大値の変数を呼び出すことでclamp()を算出できるようにしています。 計算式はLinearly Scale font-size with CSS clamp() Based on the Viewportを参考にしています。

*,
::before,
::after {
--clamp-root-font-size: 16;
--clamp-slope: calc((var(--clamp-max) - var(--clamp-min)) / (var(--clamp-viewport-max) - var(--clamp-viewport-min)));
--clamp-y-axis-intersection: calc(var(--clamp-min) - (var(--clamp-slope) * var(--clamp-viewport-min)));
--clamp-preffered-value: calc(
var(--clamp-y-axis-intersection) * (1rem / var(--clamp-root-font-size)) + (var(--clamp-slope) * 100vi)
);
--clamp: clamp(
calc(var(--clamp-min) * (1rem / var(--clamp-root-font-size))),
var(--clamp-preffered-value),
calc(var(--clamp-max) * (1rem / var(--clamp-root-font-size)))
);
font-size: var(--clamp);
}
/* bodyにデフォルト値を設定する */
body {
--clamp-viewport-min: 375;
--clamp-viewport-max: 1200;
--clamp-min: 14;
--clamp-max: 16;
}

こちらをベースのCSSで用意していただき、見出し要素を20px〜24pxで調整したい場合は

/* 👍使い方 */
.title {
--clamp-min: 20;
--clamp-max: 24;
}

とすることで.title要素のフォントサイズがclamp化されます。

難点はfont-size: var(--clamp);をユニバーサルセレクタで指定しているため、詳細度が0なところです。 リセットCSSによっては見出しやbuttonなどにfont-size: inherit;が指定されている場合があり、その場合はinheritで上書きされてしまうため変数を呼び出しただけでは機能しなくなります。 そういった場合は逐一font-size: var(--clamp);を指定し直すか、CSSのカスケードレイヤーを用いてclamp.cssの詳細度を強くする必要があります。

当ブログのglobal.cssはITCSSのレイヤーを参考にカスケードレイヤー化しており、リセットCSSに上書きされないよう設計を行っています。

@layer settings, generic, base, objects, vendors, components, utilities;
@import './settings/variables.css' layer(settings);
@import './settings/keyframes.css' layer(settings);
@import './settings/theme.css' layer(settings);
@import './generic/reset.css' layer(generic);
@import './base/clamp.css' layer(base);
/* 以下省略 */

余白や要素の幅の指定はclamp()されたfont-sizeを基準としたemで組む

コンテナ幅とか変化してほしくない箇所はpxで組み、余白(marginpadding)やサイズ(inline-sizeblock-size)の指定はclamp()されたfont-sizeを基準としたemで組むようにしました。

さらに言えばトップページのメインビジュアルの高さはblock-size: var(--clamp);で指定されています。

モバイルとデスクトップのデザインでサイズ比率を揃える必要があるので、実務でこの方法を採用するのは難しいかもしれません。しかし、この方法を使うことで、コンテナクエリによる余白の調整がほとんど不要になり、コードをシンプルにすることができました。さらに、画面の幅を狭めても余白に一貫性を持たせられ、ガタつきなどのデザイン乱れも起きにくくなるため、見た目が改善されたのも大きな利点でした。

正直CSS単位界では影の薄いemですが、clamp()と組み合わせることでレスポンシブにおけるTier1単位に化けたのは想定外でした。

375px未満はviewportを固定する

俺流レスポンシブコーディングでも紹介したviewportを固定するJSを導入しています。

import debounce from '@/scripts/utils/debounce'
const initializeViewport = () => {
const handleResize = () => {
const minWidth = 375
const value = window.outerWidth > minWidth ? 'width=device-width,initial-scale=1' : `width=${minWidth}`
const viewport = document.querySelector('meta[name="viewport"]')
if (viewport && viewport.getAttribute('content') !== value) {
viewport.setAttribute('content', value)
}
}
const debouncedResize = debounce(handleResize, 250)
window.addEventListener('resize', debouncedResize, false)
handleResize()
}
export default initializeViewport

変更点としてはdebounce()関数を使ってリサイズイベントの最適化をしているくらいです。

近頃ではガジェット系YouTuberがGalaxy Foldを持ち上げており、300px以下の画面幅で見られる可能性もあるのでこのJSの導入は保険としてマストかなと思ってます。

レスポンシブイメージのsrcset属性とsizes属性の最適化はRespImageLintに任せる

当ブログのアイキャッチや記事内の画像はレスポンシブイメージ対応を行っていますが、srcset属性とsizes属性の指定って正直良くわかっていなくてこれくらいでいいだろうという雰囲気で行っていました。

最近ページ内の画像のsrcset属性とsizes属性が適切かどうか検査してくれるブックマークレット「RespImageLint」を知り、適切でない場合には推奨値を出してくれるため、これに任せればいいじゃんという思考に至りました。

RespImageLintによるエラーメッセージのスクリーンショットです。エラーは主に、画像のサイズとそれに対して指定されたsrcset属性とsizes属性の間の不一致に関連しています。具体的には、以下のような内容です。画像の実際のサイズがsrcset属性で指定されたディスクリプタと一致しない問題です。例として、1024wのディスクリプタが512x384サイズの画像に対して使用されていますが、これは不一致を示しています。また、min-width: 980pxとmin-width: 420pxで指定されたビューポートの幅に対して、画像がそれぞれ820ピクセル、90ピクセルの幅であるべきですが、実際には614ピクセルの幅しかないため、その解決策を提案しています。
(例)RespImageLintに怒られ、数値を提案されるAbout画像

まずはそれっぽくsrcset属性とsizes属性を指定し、RespImageLintを起動すれば推奨値を出してくれるのでそれをそのまま受け入れる。こうやって当ブログのレスポンシブイメージ対応は終わりました。


皆様のレスポンシブライフの参考にしていただければ幸いです。

Share

本文上部へ戻る