CSSによるレイアウトの崩れやおかしな挙動を解決するテクニックのまとめ -Defensive CSS

WebページやUIコンポーネントのレイアウトの崩れ、おかしな挙動にあらかじめ対応しておくためのCSSのテクニックを紹介します。

FlexboxやCSS Gridによるレイアウトの崩れ、テキストが長いコンテンツ、固定の幅・固定の高さによるレイアウトの崩れ、子アイテムが増えすぎたり減りすぎたりで崩れたりなど、起こりがちな問題を解決する実践的なテクニックが満載です。

CSSによるレイアウトの崩れやおかしな挙動を解決するテクニックのまとめ -Defensive CSS

Defensive CSS
by Ahmad Shadeed

下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。

はじめに

CSSによるレイアウトの崩れやおかしな挙動が発生しないようにする方法があればいいのに、と思うことがよくあります。ご存じのとおり、コンテンツは動的なものであり、Webページ上で状況が変化する可能性があるため、CSSのによるレイアウトの崩れやおかしな挙動が発生する可能性は高くなります。

Defensive CSS(防御的CSS)とは、保護されたCSSの作成に役立つスニペットを集めたものです。つまり、将来的に問題を少なくすることができます。私のブログをご存じの方は少し前に書いた記事当ブログの翻訳記事)を読んでいるかもしれません。今回の記事はそれをもとに構築したもので、今後もスニペットのリストとして継続していく予定です。もし何か提案がありましたら、お気軽にお知らせください。

Flexboxでの折り返し

CSS Flexboxは、現在最も便利なCSSレイアウト機能の1つです。ラッパーにdisplay: flex;を与え、子アイテムを隣同士に並べるのは魅力的です。

問題はコンテナに十分なスペースがない場合、これらの子アイテムはデフォルトでは折り返されません(改行されません)。flex-wrap: wrap;でそのビヘイビアを変更する必要があります。

下記はその典型的な例です。
隣同士に並んで表示されるべきオプションのグループがあります。

デモのキャプチャ

各オプションは、隣同士に並べて表示したい

コンテナに十分なスペースがないと、横スクロールが発生します。これは予想されることなので、実際には問題ではありません。

デモのキャプチャ

コンテナに十分なスペースがないと、横スクロールが発生

アイテムがまだ隣り合っていることに注目してください。これを修正するために、flexのラッピングを許可する必要があります。

デモのキャプチャ

Flexboxでの折り返し

Flexboxを使用する際の一般的な経験則は、スクロールするラッパーを必要としない限り、ラッピングを許可することです。これとは別のことですが、予期しないレイアウトのビヘイビア(この場合は水平スクロール)を回避するためには、flex-wrapを使用します。

参考:

スペースの確保

わたし達デベロッパーは、コンテンツのさまざまな長さを考慮して実装します。つまり、一見必要がないように見えるスペースも、実装に追加する必要があります。

デモのキャプチャ

実装する時は、コンテンツのさまざまな長さを考慮する

この例では、タイトルと右側にアクションボタンがあります。上記では、問題ないように見えます。しかし、タイトルが長くなるとどうなるかを見てみましょう。

デモのキャプチャ

タイトルが長くなった場合

タイトルがアクションボタンに近すぎることに注目してください。この場合、複数行の折り返しについて考えるかもしれませんが、それについては別のセクションで説明します。ここではスペースの確保に注目します。

タイトルとアクションボタンの間にスペースが確保されていれば、このような問題は発生しません。

デモのキャプチャ

一見必要がないように見えるスペースを確保して実装する

テキストが長いコンテンツ

テキストが長いコンテンツを考慮することは、レイアウトを構築する上で重要です。前述で見たように、タイトルが長すぎる場合は切り捨てることができます。これはオプションですが、UIによってはそれを考慮することが重要な場合もあります。

私にとって、これはCSSの防御的なアプローチです。実際に問題が起こる前に問題を修正できるようにしておくのはいいことです。

ここに人名のリストがありますが、今のところ問題はないように見えます。

デモのキャプチャ

人名のリスト

ただし、これはユーザーが生成するコンテンツなので、人名が長くなってしまった場合にレイアウトを防御しておく方法に注意する必要があります。

デモのキャプチャ

人名が長い場合

このようなレイアウトでは、一貫性が重要です。そのためには、text-overflowとその仲間を使用して、テキストを切り捨てることができます。

デモのキャプチャ

テキストを切り捨てる

もしあなたがCSSで長いコンテンツを扱うスキルを磨きたいなら、私はそのトピックについて詳細な記事を書きました。

画像の伸縮を防止する

ページ上で画像のアスペクト比を制御できない場合、ユーザーがアスペクト比に合わない画像をアップロードしたときの解決策を前もって実装しておくことをお勧めします。

写真付きのカードコンポーネントがあります。見た目はいい感じです。

デモのキャプチャ

写真付きのカードコンポーネント

しかし、ユーザーが異なるサイズの画像をアップロードした場合、画像が引き伸ばされます。これはよろしくありません。

サイトのキャプチャ

画像のサイズが異なると、画像が引き伸ばされてしまう

そのための最も簡単な対策は、CSSのobject-fitを使用することです。

デモのキャプチャ

CSSのobject-fitを使用

参考:

スクロールが連鎖するのを回避

モーダルを開いてスクロールし、最後まで到達してスクロールを続けると、モーダルの下のコンテンツ(body要素)にスクロールが連鎖してしまう、という経験はないでしょうか。これはscroll chaining(スクロールの連鎖)と呼ばれるものです。

これを回避するためのテクニックがいくつかありましたが、現在ではCSSのoverscroll-behaviorプロパティのおかげで、CSSのみでこれを回避できます。

デモのキャプチャ

モーダルのスクロールが終わったら、その下のコンテンツがスクロールしてしまう

この問題を事前に回避するためには、スクロールが必要なコンポーネント(モーダル・チャットコンポーネント・スマホのメニューなど)にこの機能を追加します。このプロパティの良いところは、スクロールが行われるまでは何の影響も与えないことです。

デモのキャプチャ

スクロールが連鎖するのを回避

参考:

CSS変数のフォールバック

最近、CSS変数の使用率はますます高まっています。何らかの理由でCSS変数の値が空だった場合に備えて、エクスペリエンスを損なわない方法でそれらを使用するために適用できる方法があります。

これは、JavaScript経由でCSS変数の値を与える場合に特に便利です。

calc()内に--actions-widthという変数が使用されており、その値はJavaScriptから取得されています。JavaScriptが何らかの理由で失敗したとすると、どうなるでしょうか?
max-widthnoneになってしまいます。

それを事前に回避して、var()にフォールバック値を追加しておきます。

これでCSS変数が定義されていない場合は、フォールバック値(70px)が使用されます。この方法は変数が失敗する可能性がある場合に使用できます(JavaScriptで与える場合など)。それ以外の場合は必要ありません。

参考:

固定の幅・固定の高さによるレイアウトの崩れ

レイアウトが崩れる原因としてよくあるのが、異なる長さのコンテンツを持つ要素に固定の幅や高さを使用することです。

固定の高さによるレイアウトの崩れ

高さが固定されたヒーローセクションで、その高さよりも大きなコンテンツが表示され、レイアウトが崩れているのをよく見かけます。

デモのキャプチャ

ヒーローセクションで、コンテンツがはみ出て表示されてしまう

ヒーローセクションからコンテンツがはみ出ないようにするには、heightの代わりにmin-heightを使用します。そうすれば、コンテンツの高さが大きくなっても、レイアウトが崩れることはありません。

デモのキャプチャ

min-heightを使用する

固定の幅によるレイアウトの崩れ

ボタンのラベルが左右の端に寄りすぎているのを見たことはありませんか? これは、固定幅を使用しているのが原因です。

ボタンのラベルが100pxより長いと、端に寄ってしまいます。さらに長すぎると、ボタンからはみ出ます。これはよろしくありません!

デモのキャプチャ

ラベルが端に寄ったり、はみ出たりする

これを修正するには、widthの代わりにmin-widthを使用します。

忘れがちな背景の繰り返し

大きな画像を背景として使用する場合、大きなスクリーンでの表示を考慮することを忘れがちです。背景はデフォルトで、繰り返されます。

ノートパソコンのスクリーンでは大丈夫かもしれませんが、大きなスクリーンでははっきりと見ることができます。

デモのキャプチャ

大きいスクリーンで表示すると、背景が繰り返されている

このビヘイビアを回避するには、必ずbackground-repeatをリセットしてください。

垂直のメディアクエリ

コンポーネントを実装した後、ブラウザの幅を変更して表示をテストするでしょう。ブラウザの高さに対してテストすると、面白い問題が見つかることがあります。

私が何度も経験したことのある問題を紹介します。メインとセカンダリのリンクを持つサイドバーがあります。セカンダリリンクは常に、asideセクションの一番下に配置するとします。

下記の表示では、メインとセカンダリのリンクは問題なさそうです。私が見た例では、デベロッパーはセカンダリナビゲーションにposition: sticky;を追加して、一番下にくっつくようにしています。

デモのキャプチャ

とりあえず、表示に問題はない

しかし、ブラウザの高さが小さくなると、おかしくなります。2つのナビゲーションがどのように重なっているかに注目してください。

デモのキャプチャ

ブラウザの高さが小さいと、2つが重なる

この問題は、メディアクエリを垂直で使用することで解決します。

このCSSでビューポートの高さが600px以上の場合のみ、セカンダリナビゲーションが下にくっつくようになります。

ほかの解決方法(margin-autoを使用するなど)もありますが、ここでは垂直メディアクエリにフォーカスしました。

参考:

justify-content: space-between; を使用する

flexコンテナでは、justify-contentを使って子アイテム間のスペースを空けることがあります。子アイテムの数が一定以上であれば、レイアウトは問題なく表示されます。しかし、子アイテムが増えたり減ったりすると、レイアウトがおかしくなります。

デモのキャプチャ

justify-contentで、子アイテム間のスペースを空ける

4つの子アイテムを持つflexコンテナがあります。子アイテム間のスペースはgapmarginではなく、コンテナのjustify-content: space-between;で与えています。

子アイテムの数が4未満の場合、下記のようになります。

デモのキャプチャ

子アイテムの数が4未満の場合

これはよろしくありません。
さまざまな解決方法があります

  • marginに変更する
  • gapに変更する
  • paddingに変更する(各子要素の親に適用)
  • スペーサーとして機能する空の要素を追加する

もっとも簡単な方法は、gapに変更することです。

デモのキャプチャ

gapに変更

これでいい感じになりました。

画像の上にテキストを配置

画像の上にテキストを配置する際、画像の読み込みに失敗した場合を考慮することが重要です。テキストはどのように表示されると思いますか?

デモのキャプチャ

画像の上にテキストを配置

画像の読み込みに失敗すると、テキストは読めなくなってしまうことがあります。

デモのキャプチャ

画像の読み込みに失敗した場合

この問題は<img>要素に背景色を用意しておくことで、簡単に解決できます。この背景は、画像の読み込みに失敗したときだけ表示されます。

こうしておけば、テキストは読めます。

デモのキャプチャ

背景色を用意しておく

CSS Gridで固定値を使用する際の注意点

asidemainを含むグリッドがあるとします。CSSは下記のような感じです。

しかしこれは、小さなビューポートサイズではスペースがないため、レイアウトが崩れてしまいます。このような問題を避けるため、上記のようなCSS Fridを使用する場合は、必ずメディアクエリを使用してください。

必要な場合のみスクロールバーを表示する

幸いなことに、長いコンテンツがある場合に限り、スクロールバーを表示するかどうかを制御できます。overflowプロパティの値としてautoを使用することを強くお勧めします。

たとえば、次の例を見てましょう。

デモのキャプチャ

overflow-y: scroll;の場合

上記ではコンテンツが短くても、スクロールバーが表示されていることに注目してください。これはUIとしてよろしくありません。UIデザイナーとしては、必要のないスクロールバーが見えると混乱するだけです。

overflow-y: auto;を使用すると、コンテンツが長い場合にのみスクロールバーが表示されます。そうでない場合は表示されません。

デモのキャプチャ

overflow-y: auto;の場合

スクロールバーのガター

もうひとつスクロールに関係するのが、スクロールバーのガターです。
前述の例では、コンテンツが長くなったときにスクロールバーをつけるとレイアウトがずれてしまいました。レイアウトがずれる理由は、スクロールバー用のスペースです。

たとえば、次の例を見てましょう。

デモのキャプチャ

スクロールバー用のスペースで、レイアウトがずれてしまう

スクロールバーを表示した結果、コンテンツが長くなったときに、コンテンツが移動していることに注目してください。scrollbar-gutterプロパティを使用することで、このビヘイビアを回避できます。

デモのキャプチャ

コンテンツが短い場合にもスクロールバー用のスペースを確保しておく

参考:

CSS Flexboxにおけるコンテンツの最小サイズ

flexアイテムに、アイテム自身よりも大きなテキストや画像がある場合、ブラウザはテキストや画像を縮小しません。これはFlexboxのデフォルトのビヘイビアです。

たとえば、テキストが非常に長い場合、新しい行に折り返されることはありません。

デモのキャプチャ

テキストが長くても自動で折り返されない

この問題はoverflow-break: break-word;を使用しても、うまく機能しません。

デモのキャプチャ

overflow-break: break-word;を使用しても、機能しない

この問題を解決するには、flexアイテムのmin-width0にする必要があります。この問題の原因はmin-widthのデフォルト値がautoであるため、オーバーフローが起きてしまうからです。

同じことがflexboxのラッパーでカラムを実装する時にも当てはまります。min-heightが定義されていなくてもデフォルト値が適用されてしまうため、min-height: 0;を定義します。

デモのキャプチャ

min-height: 0;を定義しておく

CSS Gridにおけるコンテンツの最小サイズ

Flexboxと同様に、CSS Gridは子アイテムのデフォルトの最小コンテンツサイズをautoにしています。つまり、gridアイテムより大きな要素がある場合、その要素はオーバーフローします。

デモのキャプチャ

gridアイテムより大きな要素がある場合、オーバーフローする

上の例では、mainの中にカルーセルがあります。コンテキストとして、HTMLとCSSは下記の通りです。

カルーセルは折り返さないflexコンテナであるため、幅はmainよりも大きく、したがってgirdアイテムはそれを尊重します。その結果、水平方向のスクロールが表示されます。

この問題を解決する方法は、3つあります。

  • minmax()を使用する
  • gridアイテムにmin-widthを適用する
  • gridアイテムにoverflow: hidden;を適用する

防御的なCSSとしては、minmax()関数を使用することをお勧めします。

デモのキャプチャ

1frではなく、minmax(0, 1fr)にする

参考:

auto-fitとauto-fillの使い分け

CSS Gridでminmax()関数を使用する場合、auto-fitauto-fillのどちらのキーワードを使用するかを決めることが重要です。使い方を誤ると、予期せぬ結果を招くことがあります。

minmax()関数を使用した場合、auto-fitは利用可能なスペースを埋めるためにgirdアイテムを拡張します。そして、auto-fillはgirdアイテムの幅を変更せずに、利用可能なスペースを確保したままにします。

デモのキャプチャ

auto-fitauto-fillの違い

とはいえ、auto-fitを使用すると、特に予想以上にgirdアイテムの幅が広くなりすぎることがあります。次の例をご覧ください。

もしgirdアイテムが1つだけでauto-fitが使用されている場合、アイテムはコンテナの幅いっぱいに拡張されます。

デモのキャプチャ

girdアイテムが1つだけだと、幅いっぱいに拡張される

ほとんどの場合、そのようなビヘイビアは必要ないので、auto-fillを使用する方が良いと思います。

デモのキャプチャ

auto-fillを使用するとちょうど良い幅になる

参考:

画像の最大幅

基本的に、すべての画像にmax-width: 100%;を設定することを忘れないでください。これは、あなたが使用するCSSリセットに追加しておくことができます。

参考:

CSS Gridでposition: sticky;が効かないとき

gridコンテナの子にposition: sticky;を使用したことがありますか? girdアイテムのデフォルトのビヘイビアは、stretchです。このデフォルトにより、下記のようにaside要素はmain要素と同じ高さになります。

デモのキャプチャ

CSS Gridのデフォルトで、asidemainは同じ高さになる

しかし、同じ高さになることでposition: sticky;が機能しません。期待どおりに機能させるには、align-selfプロパティをリセットする必要があります。

デモのキャプチャ

align-self: start;を追加すると、position: sticky;が機能する

参考:

セレクタのグルーピング

異なるブラウザで動作するように意図されたセレクタをグループ化することは推奨されません。たとえば、inputplaceholderを定義する場合、ブラウザごとに複数のセレクタが必要です。セレクタをグループ化すると、w3cによれば、ルール全体が無効になります。

リスト内に無効なセレクタがある場合、リスト全体が無効になります。

その代わりに、下記のように記述します。

終わりに

これで終わりではありません。しかし、これらのテクニックを文書化するのは本当に楽しかったです。これは、私が個人的に取り組んでいるプロジェクトに応じて使用する防御的なCSSテクニックの継続的なリストです。
もし何かご提案があれば、@shadeed9までお願いします。

sponsors

top of page

©2022 coliss