CSSで期待通りに表示されない根本的な原因の見つけ方
Post on:2020年10月8日
CSSで、期待通りに表示されない、なんかずれてる、ブラウザによって表示が異なる、ということがありませんか?
CSSで期待通りに表示されない時に、根本的な原因を見つける方法を紹介します。
その原因がブラウザによるものなのか、CSSの仕様によるものなのか、どのように機能するか知ることで、問題の根本的な原因を見つけることができます。
Finding The Root Cause of a CSS Bug
by Ahmad Shadeed
下記は各ポイントを意訳したものです。
※当ブログでの翻訳記事は、元サイト様にライセンスを得て翻訳しています。
注意: ここでの「バグ」はエラーだけではなく、想定外の挙動・振る舞いです。
はじめに
フロントエンドの開発者であるあなたは、ユーザーが必要としていることを達成するのを妨げる可能性のあるCSSのバグにたくさん直面したことがあるでしょう。CSSのバグには視覚的なものと技術的なものがあり、ユーザーエクスペリエンスに影響を与える可能性があります。場合によっては急いでいて、修正する時間がないため、バグの見た目だけを解決する迅速な方法を採用するかもしれません。
バグの根本的な原因を見つけずに修正することは、一時的には問題ないかもしれませんが、さらに他のバグを発生させてしまうこともあります。つまり、バグを解決したのではなく、またバグを減らしたのではなく、バグを増やしたことになります。
私はCSSのデバッグ本に取り組んでいる時に、CSSのバグとデバッグについて多くのことを学びました。この記事では、バグを根本から修正するプロセスを解説し、CSSに関する一般的な問題、その問題を修正することでさらに多くの問題が発生する可能性について解説します。
CSSの基本的なバグ
CSSのさまざまなバグを解説する前に、問題を明らかにするための入門的で視覚的な例を紹介したいと思います。
Web上でよく見かけるパターンとして、カードのグリッドがあるとします。親要素(カード)の幅が等しくなるように、画像を設定します。これを実現するために、以下のCSSを画像要素に定義しました。
1 2 3 |
.card__img { width: 100%; } |
あなたはwidthの定義だけで、画像が常に同じサイズになると期待するかもしれません。
期待する表示
画像のサイズが等しければ、期待通りに同じサイズで表示されます。
しかし、アスペクト比が異なる画像を追加すると、レイアウトが簡単に壊れてしまいます。
画像のサイズが異なると、レイアウトが壊れる
これでいいのでしょうか?
レンガ状のMasonryレイアウトに似ていますが、このレイアウトの目的はカードコンポーネントを特定のサイズに制限することです。カードの高さが異なるのは、期待通りではありません。
この問題の簡単な解決策を示します。
1 2 3 4 |
.card__img { width: 100%; height: 200px; } |
画像要素に固定の高さを追加すると、画像のサイズが同じに見えるようになりますが、残念ながらこれは別の問題が発生します。画像の高さが大きい場合、圧縮されたように表示されてしまいます。
画像の高さが大きい場合、圧縮されたように表示される
heightを追加することで問題が部分的に解決され、カードコンポーネントの高さは同じになりました。しかし、この解決方法は垂直画像の圧縮というさらに悪い問題を引き起こしました。フロントエンドの開発者は、コンポーネントがどのように動作し、どのように振る舞うべきかを正確に理解できるようにするため、多くの質問をする責任があります。
質問や考慮すべき点は例えば、
- カードコンポーネントの高さは同じにする必要がありますか?
- カードのサムネイル画像の予想サイズは?
- コンテンツ作成者が小さい画像または大きい画像をアップロードできるようにする必要がありますか?
これらの答えは、プロジェクトによって異なります。
本物のプロジェクトの一例として、ここでは私が答えます。
- はい、それらは同じ高さでなければなりません。
- 330px * 220pxです。
- いいえ、画像サイズは一定でなければなりません。
カードコンポーネントの振る舞いが明確になったので、この問題の適切な修正方法を見てましょう(回答が異なれば、修正方法も異なります)。画像要素にobject-fit: cover;を適用することで、画像が圧縮されないようにできます。
1 2 3 4 5 |
.card__img { width: 100%; height: 220px; object-fit: cover; } |
画像のサイズが同じであることは分かっていますが、object-fitを追加することで、将来起きるかもしれない望ましくない問題も回避できます。そして最後に、考えなければならない質問は、画像の一部を隠してもよいのかということです。object-fitのおかげで画像は圧縮されませんが、指定した幅や高さに収まるように画像のサイズを変更するため、画像の一部が隠れてしまうかもしれません。
object-fit: cover;が適用された画像を見てください。
object-fit: cover;が適用された画像
右の画像の一部が隠されていることに注目してください。これは想定通りの挙動で、object-positionプロパティを加えることで解決できます。
1 2 3 4 5 6 |
.card__img { width: 100%; height: 220px; object-fit: cover; object-position: center 75%; } |
上記で問題は解決しますが、実用的ではありません。なぜなら、画像ごとにobject-positionの値を変更しなければならないからです。
object-position: center 75%;を追加
撮影や編集に多額の費用をかけているのだから、画像の一部を隠すべきではないという意見もあるでしょう。そのような場合、あなたが唯一できることは、自分の問題ではないことを説明し、画像のサイズはすべて統一させるべきであることを説明するしかありません。
基本的な例を説明したので、根本的な原因を見つけるというコンセプトを明確にするために、さらに例を見てみましょう。
ドロップダウンメニューの配置
今までにあなたは、ヘッダーコンポーネントにカスタムドロップダウンメニューを組み込んだことがありましたか? おそらく、実装したことがあるでしょう。
ドロップダウンメニューの配置
ドロップダウンメニューはナビゲーションに重なるように配置されます。配置するには、ドロップダウンは絶対配置の要素なので、topプロパティで簡単にできます。
1 2 3 4 5 6 7 8 9 |
.nav__item { position: relative; } .nav__dropdown { position: absolute; left: 0; top: 40px; } |
このCSSでドロップダウンは親ナビゲーションの上端から40pxに配置されます。これには問題はありませんが、将来的に問題が発生する可能性があります。「ヘッダの高さを小さくしてほしい」という要望があったとします。ナビゲーションの実装方法にもよりますが、ナビゲーションアイテムの垂直パディングを減らすか、ヘッダ自体の高さを小さくする方法があります。
ヘッダの高さを要望通り小さくしたら、ドロップダウンメニューも期待通りに機能するかテストすることを忘れないでください。残念ながら、前述の実装方法では期待通りに機能しません。
ドロップダウンメニューの配置が期待通りにならない
配置が期待通りにならない原因は、topプロパティに固定値を使用したためです。これを行うより良い実装方法は、%値を使用することです。これにより、ナビゲーションの高さに関係なく、ドロップダウンはその場所に留まります。
1 2 3 4 5 |
.nav__dropdown { position: absolute; left: 0; top: 100%; } |
CSSの基本をよく理解することで、このような状況を回避できます。topプロパティに固定値を使用するのは仕様的には間違いではありませんが、頼りにはなりません。
HTMLの置換要素
MDNによると、
置換された要素とは、そのコンテンツが現在のドキュメントのスタイルの影響を受けない要素のことです。置換された要素の位置はCSSで影響を与えることができますが、置換された要素のコンテンツ自体は影響を受けません。
絶対配置の画像
置換された要素の例として、HTMLの<img>があります。これについてあまり知られていない事実を探ってみたいと思います。
例えば、widthとheightを定義せずに、コンテナに対して絶対配置された画像があるとします。
運良く画像のサイズがラッパーと同じであれば、これから説明する問題に気がつかないでしょう。
1 2 3 4 5 6 7 |
.card__image { position: absolute; left: 0; top: 0; right: 0; bottom: 0; } |
ラッパーの四辺から0の値で配置されているため、画像が親要素を埋めるように見えるかもしれません。
いいえ、そうではありません!
四辺から0の値で配置
画像が親要素の外に出ていることに注目してください。
この理由は、画像が置換された要素であり、そのデフォルトの幅がコンテンツのサイズに基づいているためです。そのため、widthとheightプロパティを定義する必要があります。
1 2 3 4 5 6 7 |
.card__image { position: absolute; left: 0; top: 0; width: 100%; height: 100%; } |
これは複雑な問題ではありませんが、開発者がこのことを知っていないと、非常に悩むかもしれません。
widthとheightを定義すると解決
Flexアイテムとしての画像
Flexアイテムとしての画像
複数の画像を含むラッパーが必要な場合は、Flexboxで処理するのも難しい場合があります。以下のようなHTMLとCSSを想定してみてください。
1 2 3 4 |
<div class="wrapper"> <img src="image-1.jpg" alt="" /> <img src="image-2.jpg" alt="" /> </div> |
1 2 3 4 5 6 7 |
.wrapper { display: flex; } .wrapper img { flex: 1; } |
上記は、画像がラッパーや親要素よりも小さい場合には問題なく機能します。この問題を解決するためにいろいろ試してみることができますが、CSSの仕様を読むまでは、本当の原因が分からないでしょう。
デフォルトでは、Flexアイテムは最小コンテンツサイズ(最長の単語または固定サイズ要素の長さ)未満には縮小されません。これを変更するにはmin-widthまたはmin-heightプロパティを設定します。
つまり、Flexアイテムをmin-widthで強制的に最小コンテンツサイズ以下に縮小させる必要があります(この例ではflex-directionがrowのため)。
1 2 3 4 5 6 7 8 |
.wrapper { display: flex; } .wrapper img { flex: 1; min-width: 0; } |
これで修正完了です!
CSSの仕様を理解し、裏でどのように機能するかを知るだけで、問題の根本的な原因を知ることができました。
flexプロパティの詳細については、flexプロパティの実践的な使い方を徹底解説をご覧ください。
オーバーフロー要素とインラインブロック要素
この考え方については、Overflow in CSSで説明しました。
左端のボタンだけにoverflow: hidden;を定義
3つのinline-block要素があり、そのうちの1つにoverflow: hidden;が定義されています。overflowがhiddenになっているボタンは、他の兄弟のベースラインと揃っていないことに気づくと思います。
CSSの仕様によると、
inline-blockのベースラインは、インフロー(in-flow)のラインボックスがない場合や、overflowプロパティの値がvisible以外の値がある場合を除き、通常のフロー内での最後のラインボックスのベースラインになります。
幸運なことに、Bootstrapのリポジトリでこの問題を実際に見ることができるとGaël Poupardが教えてくれました。
検索結果ページでは、クエリの文字列は1単語から1文までさまざまです。何らかの理由で以下のデザインでは、文字列を特定の幅に制限する必要があります。これを実現するために、制作者は以下のCSSを記述しています。
1 2 3 4 5 6 7 |
.search-results span { display: inline-block; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
このCSSには、問題があります。
クエリの文字列はその兄弟要素と整列されていません。下図をご覧ください。
Bootstrapのリポジトリのキャプチャ
CSSを十分に理解していないと、これはブラウザ関連の問題だと思うかもしれません。実際にこれは、当たり前のことです。主要なブラウザで表示してみましょう。
3つのブラウザで表示
ChromeとFirefoxで同じように表示され、Safariのみ違う結果が表示されています。これを見て、こんな風に思うかもしれません。「Safariでは正常に機能しているので、問題はChromeとFirefoxにある」と。しかし、その後何時間もかけて調べ、実はSafariだけが正常ではないことに気づくのです。
さらに、仕事を早く終わらせたいと思っている怠け者の開発者は、マージンを追加して帰ってしまうかもしれません。下記は、悪い解決策です。
1 2 3 4 5 6 7 8 9 |
.search-results span { position: relative; top: 3px; display: inline-block; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
根本的な原因が分かったので、適切な解決策を下記に示します。
1 2 3 4 5 6 7 8 |
.search-results span { display: inline-block; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: top; } |
vertical-align: top;を追加しただけで、問題は即座に修正されます。
これが適切な解決策
インラインブロック兄弟間のスペース
インラインブロック兄弟間のスペース
コーディングが完了したら、デザイナーから「ボタンのスペースが一貫していない」というフィードバックを受けました。16pxではなく、22pxになっているところがあります。
CSSを確認してみると、下記のようになっていました。
1 2 3 4 |
<div class="wrapper"> <a class="button" href="">Save changes</a> <a class="button" href="">Cancel</a> </div> |
1 2 3 4 5 |
.button { display: inline-block; margin-right: 16px; /* Other button styles */ } |
スペースは、margin-right: 16px;と定義しましたが、ブラウザで表示すると、それ以上になってしまいます。急いでいる場合は、margin-rightの量を減らして、10pxにするだけで、ブラウザ上では16pxになるかもしれません。そして、運がよければ、デザイナーにOKをもらえるかもしれませんが、これは悪い解決策です。
この原因は、ボタンがインライン要素であり、ブラウザがボタンをテキスト文字として扱っているからです。テキスト文字の間にはスペースがあります。これを修正する直接的な方法はなく、解決策のほとんどはハックです。CSS-Tricksの記事でこれについて読むことができます。
この問題を解決するには、ボタンをFlexラッパーとして親にしてしまえば、スペースはハックなしで期待通りに機能します。
1 2 3 4 5 6 7 8 9 |
.wrapper { display: flex; flex-wrap: wrap; } .button { margin-right: 16px; /* Other button styles */ } |
まとめ
CSSバグの根本的な原因を見つけることは、フロントエンド開発者として重要です。問題を根本から解決したときに得られるものは以下の通りです。
- 新しいことを学ぶことができます。
- 解決策を忘れることはないでしょう。
- 経験値が上がります。
- そして、楽しいです!
CSSバグを発見して解決する方法をすべて挙げられればいいのですが、すべてを記事にすることはできません。が、私はDebugging CSSの本を書いています。興味のある人は、ニュースレーターに登録して最新の情報をゲットしてください。
コメントや提案があれば、@shadeed9までお願いします。
sponsors