CSSによるレイアウトの崩れやおかしな挙動を解決するテクニックのまとめ -Defensive CSS
Post on:2022年1月20日
WebページやUIコンポーネントのレイアウトの崩れ、おかしな挙動にあらかじめ対応しておくためのCSSのテクニックを紹介します。
FlexboxやCSS Gridによるレイアウトの崩れ、テキストが長いコンテンツ、固定の幅・固定の高さによるレイアウトの崩れ、子アイテムが増えすぎたり減りすぎたりで崩れたりなど、起こりがちな問題を解決する実践的なテクニックが満載です。
Defensive CSS
by Ahmad Shadeed
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
- はじめに
- Flexboxでの折り返し
- スペースの確保
- テキストが長いコンテンツ
- 画像の伸縮を防止する
- スクロールが連鎖するのを回避
- CSS変数のフォールバック
- 固定の幅・固定の高さによるレイアウトの崩れ
- 忘れがちな背景の繰り返し
- 垂直のメディアクエリ
- justify-content: space-between; を使用する
- 画像の上にテキストを配置
- CSS Gridで固定値を使用する際の注意点
- 必要な場合のみスクロールバーを表示する
- スクロールバーのガター
- CSS Flexboxにおけるコンテンツの最小サイズ
- CSS Gridにおけるコンテンツの最小サイズ
- auto-fitとauto-fillの使い分け
- 画像の最大幅
- CSS Gridでposition: sticky;が効かないとき
- セレクタのグルーピング
- 終わりに
はじめに
CSSによるレイアウトの崩れやおかしな挙動が発生しないようにする方法があればいいのに、と思うことがよくあります。ご存じのとおり、コンテンツは動的なものであり、Webページ上で状況が変化する可能性があるため、CSSのによるレイアウトの崩れやおかしな挙動が発生する可能性は高くなります。
Defensive CSS(防御的CSS)とは、保護されたCSSの作成に役立つスニペットを集めたものです。つまり、将来的に問題を少なくすることができます。私のブログをご存じの方は少し前に書いた記事(当ブログの翻訳記事)を読んでいるかもしれません。今回の記事はそれをもとに構築したもので、今後もスニペットのリストとして継続していく予定です。もし何か提案がありましたら、お気軽にお知らせください。
Flexboxでの折り返し
CSS Flexboxは、現在最も便利なCSSレイアウト機能の1つです。ラッパーにdisplay: flex;
を与え、子アイテムを隣同士に並べるのは魅力的です。
問題はコンテナに十分なスペースがない場合、これらの子アイテムはデフォルトでは折り返されません(改行されません)。flex-wrap: wrap;
でそのビヘイビアを変更する必要があります。
下記はその典型的な例です。
隣同士に並んで表示されるべきオプションのグループがあります。
1 2 3 |
.options-list { display: flex; } |
各オプションは、隣同士に並べて表示したい
コンテナに十分なスペースがないと、横スクロールが発生します。これは予想されることなので、実際には問題ではありません。
コンテナに十分なスペースがないと、横スクロールが発生
アイテムがまだ隣り合っていることに注目してください。これを修正するために、flexのラッピングを許可する必要があります。
1 2 3 4 |
.options-list { display: flex; flex-wrap: wrap; } |
Flexboxでの折り返し
Flexboxを使用する際の一般的な経験則は、スクロールするラッパーを必要としない限り、ラッピングを許可することです。これとは別のことですが、予期しないレイアウトのビヘイビア(この場合は水平スクロール)を回避するためには、flex-wrap
を使用します。
参考:
スペースの確保
わたし達デベロッパーは、コンテンツのさまざまな長さを考慮して実装します。つまり、一見必要がないように見えるスペースも、実装に追加する必要があります。
実装する時は、コンテンツのさまざまな長さを考慮する
この例では、タイトルと右側にアクションボタンがあります。上記では、問題ないように見えます。しかし、タイトルが長くなるとどうなるかを見てみましょう。
タイトルが長くなった場合
タイトルがアクションボタンに近すぎることに注目してください。この場合、複数行の折り返しについて考えるかもしれませんが、それについては別のセクションで説明します。ここではスペースの確保に注目します。
タイトルとアクションボタンの間にスペースが確保されていれば、このような問題は発生しません。
一見必要がないように見えるスペースを確保して実装する
テキストが長いコンテンツ
テキストが長いコンテンツを考慮することは、レイアウトを構築する上で重要です。前述で見たように、タイトルが長すぎる場合は切り捨てることができます。これはオプションですが、UIによってはそれを考慮することが重要な場合もあります。
私にとって、これはCSSの防御的なアプローチです。実際に問題が起こる前に問題を修正できるようにしておくのはいいことです。
ここに人名のリストがありますが、今のところ問題はないように見えます。
人名のリスト
ただし、これはユーザーが生成するコンテンツなので、人名が長くなってしまった場合にレイアウトを防御しておく方法に注意する必要があります。
人名が長い場合
このようなレイアウトでは、一貫性が重要です。そのためには、text-overflow
とその仲間を使用して、テキストを切り捨てることができます。
1 2 3 4 5 |
.username { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } |
テキストを切り捨てる
もしあなたがCSSで長いコンテンツを扱うスキルを磨きたいなら、私はそのトピックについて詳細な記事を書きました。
画像の伸縮を防止する
ページ上で画像のアスペクト比を制御できない場合、ユーザーがアスペクト比に合わない画像をアップロードしたときの解決策を前もって実装しておくことをお勧めします。
写真付きのカードコンポーネントがあります。見た目はいい感じです。
写真付きのカードコンポーネント
しかし、ユーザーが異なるサイズの画像をアップロードした場合、画像が引き伸ばされます。これはよろしくありません。
画像のサイズが異なると、画像が引き伸ばされてしまう
そのための最も簡単な対策は、CSSのobject-fit
を使用することです。
1 2 3 |
.card__thumb { object-fit: cover; } |
CSSのobject-fit
を使用
参考:
スクロールが連鎖するのを回避
モーダルを開いてスクロールし、最後まで到達してスクロールを続けると、モーダルの下のコンテンツ(body
要素)にスクロールが連鎖してしまう、という経験はないでしょうか。これはscroll chaining(スクロールの連鎖)と呼ばれるものです。
これを回避するためのテクニックがいくつかありましたが、現在ではCSSのoverscroll-behavior
プロパティのおかげで、CSSのみでこれを回避できます。
モーダルのスクロールが終わったら、その下のコンテンツがスクロールしてしまう
この問題を事前に回避するためには、スクロールが必要なコンポーネント(モーダル・チャットコンポーネント・スマホのメニューなど)にこの機能を追加します。このプロパティの良いところは、スクロールが行われるまでは何の影響も与えないことです。
1 2 3 4 |
.modal__content { overscroll-behavior-y: contain; overflow-y: auto; } |
スクロールが連鎖するのを回避
参考:
CSS変数のフォールバック
最近、CSS変数の使用率はますます高まっています。何らかの理由でCSS変数の値が空だった場合に備えて、エクスペリエンスを損なわない方法でそれらを使用するために適用できる方法があります。
これは、JavaScript経由でCSS変数の値を与える場合に特に便利です。
1 2 3 |
.message__bubble { max-width: calc(100% - var(--actions-width)); } |
calc()
内に--actions-width
という変数が使用されており、その値はJavaScriptから取得されています。JavaScriptが何らかの理由で失敗したとすると、どうなるでしょうか?
max-width
はnone
になってしまいます。
それを事前に回避して、var()
にフォールバック値を追加しておきます。
1 2 3 |
.message__bubble { max-width: calc(100% - var(--actions-width, 70px)); } |
これでCSS変数が定義されていない場合は、フォールバック値(70px
)が使用されます。この方法は変数が失敗する可能性がある場合に使用できます(JavaScriptで与える場合など)。それ以外の場合は必要ありません。
参考:
固定の幅・固定の高さによるレイアウトの崩れ
レイアウトが崩れる原因としてよくあるのが、異なる長さのコンテンツを持つ要素に固定の幅や高さを使用することです。
固定の高さによるレイアウトの崩れ
高さが固定されたヒーローセクションで、その高さよりも大きなコンテンツが表示され、レイアウトが崩れているのをよく見かけます。
1 2 3 |
.hero { height: 350px; } |
ヒーローセクションで、コンテンツがはみ出て表示されてしまう
ヒーローセクションからコンテンツがはみ出ないようにするには、height
の代わりにmin-height
を使用します。そうすれば、コンテンツの高さが大きくなっても、レイアウトが崩れることはありません。
1 2 3 |
.hero { min-height: 350px; } |
min-height
を使用する
固定の幅によるレイアウトの崩れ
ボタンのラベルが左右の端に寄りすぎているのを見たことはありませんか? これは、固定幅を使用しているのが原因です。
1 2 3 |
.button { width: 100px; } |
ボタンのラベルが100px
より長いと、端に寄ってしまいます。さらに長すぎると、ボタンからはみ出ます。これはよろしくありません!
ラベルが端に寄ったり、はみ出たりする
これを修正するには、min-width
を使用します。
1 2 3 |
.button { min-width: 100px; } |
忘れがちな背景の繰り返し
大きな画像を背景として使用する場合、大きなスクリーンでの表示を考慮することを忘れがちです。背景はデフォルトで、繰り返されます。
ノートパソコンのスクリーンでは大丈夫かもしれませんが、大きなスクリーンでははっきりと見ることができます。
大きいスクリーンで表示すると、背景が繰り返されている
このビヘイビアを回避するには、必ずbackground-repeat
をリセットしてください。
1 2 3 4 |
.hero { background-image: url('..'); background-repeat: no-repeat; } |
垂直のメディアクエリ
コンポーネントを実装した後、ブラウザの幅を変更して表示をテストするでしょう。ブラウザの高さに対してテストすると、面白い問題が見つかることがあります。
私が何度も経験したことのある問題を紹介します。メインとセカンダリのリンクを持つサイドバーがあります。セカンダリリンクは常に、aside
セクションの一番下に配置するとします。
下記の表示では、メインとセカンダリのリンクは問題なさそうです。私が見た例では、デベロッパーはセカンダリナビゲーションにposition: sticky;
を追加して、一番下にくっつくようにしています。
とりあえず、表示に問題はない
しかし、ブラウザの高さが小さくなると、おかしくなります。2つのナビゲーションがどのように重なっているかに注目してください。
ブラウザの高さが小さいと、2つが重なる
この問題は、メディアクエリを垂直で使用することで解決します。
1 2 3 4 5 6 |
@media (min-height: 600px) { .aside__secondary { position: sticky; bottom: 0; } } |
このCSSでビューポートの高さが600px
以上の場合のみ、セカンダリナビゲーションが下にくっつくようになります。
ほかの解決方法(margin-auto
を使用するなど)もありますが、ここでは垂直メディアクエリにフォーカスしました。
参考:
justify-content: space-between; を使用する
flexコンテナでは、justify-content
を使って子アイテム間のスペースを空けることがあります。子アイテムの数が一定以上であれば、レイアウトは問題なく表示されます。しかし、子アイテムが増えたり減ったりすると、レイアウトがおかしくなります。
justify-content
で、子アイテム間のスペースを空ける
4つの子アイテムを持つflexコンテナがあります。子アイテム間のスペースはgap
やmargin
ではなく、コンテナのjustify-content: space-between;
で与えています。
1 2 3 4 5 |
.wrapper { display: flex; flex-wrap: wrap; justify-content: space-between; } |
子アイテムの数が4未満の場合、下記のようになります。
子アイテムの数が4未満の場合
これはよろしくありません。
さまざまな解決方法があります
margin
に変更するgap
に変更するpadding
に変更する(各子要素の親に適用)- スペーサーとして機能する空の要素を追加する
もっとも簡単な方法は、gap
に変更することです。
1 2 3 4 5 |
.wrapper { display: flex; flex-wrap: wrap; gap: 1rem; } |
gap
に変更
これでいい感じになりました。
画像の上にテキストを配置
画像の上にテキストを配置する際、画像の読み込みに失敗した場合を考慮することが重要です。テキストはどのように表示されると思いますか?
画像の上にテキストを配置
画像の読み込みに失敗すると、テキストは読めなくなってしまうことがあります。
画像の読み込みに失敗した場合
この問題は<img>
要素に背景色を用意しておくことで、簡単に解決できます。この背景は、画像の読み込みに失敗したときだけ表示されます。
1 2 3 |
.card__img { background-color: grey; } |
こうしておけば、テキストは読めます。
背景色を用意しておく
CSS Gridで固定値を使用する際の注意点
aside
とmain
を含むグリッドがあるとします。CSSは下記のような感じです。
1 2 3 4 5 |
.wrapper { display: grid; grid-template-columns: 250px 1fr; gap: 1rem; } |
しかしこれは、小さなビューポートサイズではスペースがないため、レイアウトが崩れてしまいます。このような問題を避けるため、上記のようなCSS Fridを使用する場合は、必ずメディアクエリを使用してください。
1 2 3 4 5 6 7 |
@media (min-width: 600px) { .wrapper { display: grid; grid-template-columns: 250px 1fr; gap: 1rem; } } |
必要な場合のみスクロールバーを表示する
幸いなことに、長いコンテンツがある場合に限り、スクロールバーを表示するかどうかを制御できます。overflow
プロパティの値としてauto
を使用することを強くお勧めします。
たとえば、次の例を見てましょう。
overflow-y: scroll;
の場合
上記ではコンテンツが短くても、スクロールバーが表示されていることに注目してください。これはUIとしてよろしくありません。UIデザイナーとしては、必要のないスクロールバーが見えると混乱するだけです。
1 2 3 |
.element { overflow-y: auto; } |
overflow-y: auto;
を使用すると、コンテンツが長い場合にのみスクロールバーが表示されます。そうでない場合は表示されません。
overflow-y: auto;
の場合
スクロールバーのガター
もうひとつスクロールに関係するのが、スクロールバーのガターです。
前述の例では、コンテンツが長くなったときにスクロールバーをつけるとレイアウトがずれてしまいました。レイアウトがずれる理由は、スクロールバー用のスペースです。
たとえば、次の例を見てましょう。
スクロールバー用のスペースで、レイアウトがずれてしまう
スクロールバーを表示した結果、コンテンツが長くなったときに、コンテンツが移動していることに注目してください。scrollbar-gutter
プロパティを使用することで、このビヘイビアを回避できます。
1 2 3 |
.element { scrollbar-gutter: stable; } |
コンテンツが短い場合にもスクロールバー用のスペースを確保しておく
参考:
CSS Flexboxにおけるコンテンツの最小サイズ
flexアイテムに、アイテム自身よりも大きなテキストや画像がある場合、ブラウザはテキストや画像を縮小しません。これはFlexboxのデフォルトのビヘイビアです。
1 2 3 |
.card { display: flex; } |
たとえば、テキストが非常に長い場合、新しい行に折り返されることはありません。
テキストが長くても自動で折り返されない
この問題はoverflow-break: break-word;
を使用しても、うまく機能しません。
1 2 3 |
.card__title { overflow-wrap: break-word; } |
overflow-break: break-word;
を使用しても、機能しない
この問題を解決するには、flexアイテムのmin-width
を0
にする必要があります。この問題の原因はmin-width
のデフォルト値がauto
であるため、オーバーフローが起きてしまうからです。
1 2 3 4 |
.card__title { overflow-wrap: break-word; min-width: 0; } |
同じことがflexboxのラッパーでカラムを実装する時にも当てはまります。min-height
が定義されていなくてもデフォルト値が適用されてしまうため、min-height: 0;
を定義します。
min-height: 0;
を定義しておく
CSS Gridにおけるコンテンツの最小サイズ
Flexboxと同様に、CSS Gridは子アイテムのデフォルトの最小コンテンツサイズをauto
にしています。つまり、gridアイテムより大きな要素がある場合、その要素はオーバーフローします。
gridアイテムより大きな要素がある場合、オーバーフローする
上の例では、main
の中にカルーセルがあります。コンテキストとして、HTMLとCSSは下記の通りです。
1 2 3 4 5 6 |
<div class="wrapper"> <main> <section class="carousel"></section> </main> <aside></aside> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 |
@media (min-width: 1020px) { .wrapper { display: grid; grid-template-columns: 1fr 248px; grid-gap: 40px; } } .carousel { display: flex; overflow-x: auto; } |
カルーセルは折り返さないflexコンテナであるため、幅はmain
よりも大きく、したがってgirdアイテムはそれを尊重します。その結果、水平方向のスクロールが表示されます。
この問題を解決する方法は、3つあります。
minmax()
を使用する- gridアイテムに
min-width
を適用する - gridアイテムに
overflow: hidden;
を適用する
防御的なCSSとしては、minmax()
関数を使用することをお勧めします。
1 2 3 4 5 6 7 |
@media (min-width: 1020px) { .wrapper { display: grid; grid-template-columns: minmax(0, 1fr) 248px; grid-gap: 40px; } } |
1fr
ではなく、minmax(0, 1fr)
にする
参考:
auto-fitとauto-fillの使い分け
CSS Gridでminmax()
関数を使用する場合、auto-fit
とauto-fill
のどちらのキーワードを使用するかを決めることが重要です。使い方を誤ると、予期せぬ結果を招くことがあります。
minmax()
関数を使用した場合、auto-fit
は利用可能なスペースを埋めるためにgirdアイテムを拡張します。そして、auto-fill
はgirdアイテムの幅を変更せずに、利用可能なスペースを確保したままにします。
auto-fit
とauto-fill
の違い
とはいえ、auto-fit
を使用すると、特に予想以上にgirdアイテムの幅が広くなりすぎることがあります。次の例をご覧ください。
1 2 3 4 5 |
.wrapper { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-gap: 1rem; } |
もしgirdアイテムが1つだけでauto-fit
が使用されている場合、アイテムはコンテナの幅いっぱいに拡張されます。
girdアイテムが1つだけだと、幅いっぱいに拡張される
ほとんどの場合、そのようなビヘイビアは必要ないので、auto-fill
を使用する方が良いと思います。
auto-fill
を使用するとちょうど良い幅になる
参考:
画像の最大幅
基本的に、すべての画像にmax-width: 100%;
を設定することを忘れないでください。これは、あなたが使用するCSSリセットに追加しておくことができます。
1 2 3 4 |
img { max-width: 100%; object-fit: cover; } |
参考:
CSS Gridでposition: sticky;が効かないとき
gridコンテナの子にposition: sticky;
を使用したことがありますか? girdアイテムのデフォルトのビヘイビアは、stretch
です。このデフォルトにより、下記のようにaside
要素はmain
要素と同じ高さになります。
CSS Gridのデフォルトで、aside
とmain
は同じ高さになる
しかし、同じ高さになることでposition: sticky;
が機能しません。期待どおりに機能させるには、align-self
プロパティをリセットする必要があります。
1 2 3 4 5 |
aside { align-self: start; position: sticky; top: 1rem; } |
align-self: start;
を追加すると、position: sticky;
が機能する
参考:
セレクタのグルーピング
異なるブラウザで動作するように意図されたセレクタをグループ化することは推奨されません。たとえば、input
のplaceholder
を定義する場合、ブラウザごとに複数のセレクタが必要です。セレクタをグループ化すると、w3cによれば、ルール全体が無効になります。
1 2 3 4 5 |
/* これはダメな例です */ input::-webkit-input-placeholder, input:-moz-placeholder { color: #222; } |
リスト内に無効なセレクタがある場合、リスト全体が無効になります。
その代わりに、下記のように記述します。
1 2 3 4 5 6 7 |
input::-webkit-input-placeholder { color: #222; } input:-moz-placeholder { color: #222; } |
終わりに
これで終わりではありません。しかし、これらのテクニックを文書化するのは本当に楽しかったです。これは、私が個人的に取り組んでいるプロジェクトに応じて使用する防御的なCSSテクニックの継続的なリストです。
もし何かご提案があれば、@shadeed9までお願いします。
sponsors