表示件数と高さが変わるブロックの一覧を、画面サイズに合わせてカラム数を変えて表示する方法
レスポンシブなサイトで高さが可変のブロック要素の一覧をレイアウトする際にてこずったので、課題になったこととその解決方をメモしておきます。ニュースサイトなどに良くあるサムネイル画像とタイトルのブロック要素の一覧で、画面サイズに合わせてカラム数が変わる以下のようなレイアウトです。ちなみに、Flexboxを使わないやり方です。nth-child()とclearを使った方法、inline-blockを使った方法、さらに、flexboxを使った方法の3つをまとめてみました。
レイアウトの条件
レイアウトの条件を整理しておきます。
- 表示件数が変わる
- 表示件数が変わる場合でも同じHTMLとCSSでレイアウトする
- ブロックによって高さが異なる
- 画面サイズに合わせてカラム数を変える
上の条件だとli
要素をfloat
してメディア・クエリで画面ごとに幅を指定すれば簡単にできそうです。ところが、ブロックの高さが変わるのでそう簡単にはいきません。
特定の画面サイズでレイアウトが崩れてしまう
例えば、大きい画面では3カラム、中くらいの画面では2カラム、小さい画面では1カラムにするレイアウトで、li
要素をfloat
を使ってスタイルすると、特定の画面サイズでレイアウトが崩れてしまいます。しかも、下のイメージのように3カラム表示の際にレイアウトが崩れる部分と2カラム表示の際に崩れる部分が異なります。
3カラム表示
4番目のブロックが2番目のブロックに引っかかってレイアウトが崩れてしまう。
2カラム表示
今度は7番目のブロックが5番目のブロックに引っかかってレイアウトが崩れてしまう。本来意図したレイアウトでは、7番目のブロックが左寄せになります。
HTMLとCSSの例
上のようなレイアウトの崩れは、以下のようなHTMLとCSSの記述で起こります。
HTML
li
要素の中にサムネイル画像と文字数の違う記事のタイトルが入っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<ul class="block-grid block-grid-1-2-3"> <li> <img src="http://placehold.it/600x360?text=1" /> <a href="">記事のタイトルなどのテキスト要素</a> </li> <li> <img src="http://placehold.it/600x360?text=2" /> <a href="">記事のタイトルなどのテキスト要素なので、画面の幅によって改行が生じます</a> </li> <li> <img src="http://placehold.it/600x360?text=3" /> <a href="">記事のタイトルなどのテキスト要素</a> </li> ... </ul> |
CSS
以下の例では、〜600pxまでの画面サイズでは1カラム、600〜800pxの画面サイズでは2カラム、800px以上の画面サイズでは3カラムで表示するように、メディア・クエリを使って各画面サイズのスタイルでli
要素に幅を指定しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
.block-grid { width: auto; overflow: hidden; list-style: none; margin: 0 -1% 30px; padding: 0; } .block-grid li { float: left; margin: 0 1% 30px; padding: 0; } .block-grid a { text-decoration: none; font-size: 1.6em; } @media (min-width: 600px){ .block-grid-1-2-3 li { width: 48%; } } @media (min-width: 800px){ .block-grid-1-2-3 li { width: 31.3333%; } } |
nth-child()セレクタとclearプロパティを使う方法
3カラムと2カラムの表示スタイルを指定しているメディア・クエリ部分に、以下を追加するだけで対応ができます。以下のスタイルを追加することで、各カラムの最初の要素にclear: both
を指定して回り込みを解除しています。
600px以上の画面用のスタイルに追加
600〜800pxでは2カラムで表示しているので、それぞれのカラムの1つ目のブロックになる1、3、5、7番目の要素にnth-child(2n+1)
を使ってclear: both
を指定します。
1 2 3 |
.block-grid-1-2-3 li:nth-child(2n+1) { clear: both; } |
800px以上の画面用のスタイルに追加
800px以上の画面では3カラムで表示しているので、それぞれのカラムの1つ目のブロックになる1、4、7番目の要素にnth-child(3n+1)
を使ってclear: both
を指定します。そして、600〜800pxの画面様に指定した回り込み解除を無効にするためにnth-child(2n+1)
にclear: none
を指定します。この際、2と3の公倍数で回り込みの解除を無効にしないようにnth-child(3n+1)
の方を後に記述します。
1 2 3 4 5 6 |
.block-grid-1-2-3 li:nth-child(2n+1) { clear: none; } .block-grid-1-2-3 li:nth-child(3n+1) { clear: both; } |
メディア・クエリ部分の全CSS
最終的に、メディア・クエリ部分のCSSは以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@media (min-width: 600px){ .block-grid-1-2-3 li { width: 48%; } .block-grid-1-2-3 li:nth-child(2n+1) { clear: both; } } @media (min-width: 800px){ .block-grid-1-2-3 li { width: 31.3333%; } .block-grid-1-2-3 li:nth-child(2n+1) { clear: none; } .block-grid-1-2-3 li:nth-child(3n+1) { clear: both; } } |
これで、どの画面サイズでもレイアウトが崩れないようになります。
nth-child()のブラウザサポートについて
nth-child()
セレクタは、IE9以上とモダンブラウザではサポート されています。IE8以下で対応が必要な場合は、JavaScriptなどで対応する必要があります。そもそも、IE8ではメディア・クエリもサポートされてないですし、対応する状況はあまりないかもしれませんが。
inline-blockを使う方法
inline-blockを使ったやり方のほうがいいかも?
以下のようにli
要素の間のスペースを削除しないとレイアウトが崩れてしまいますが、それを除けばinline-block
を使った方法のほうがシンプルでいいかもしれません。(まだブラウザでしっかり検証できてないですが。。。)
以下のように、コメントアウトをするなどしてli
要素間のスペースを削除しないとレイアウトが崩れてしまうのでご注意を。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<ul class="block-grid block-grid-1-2-3"> <li> <img src="http://placehold.it/600x360?text=1" /> <a href="">記事のタイトルなどのテキスト要素 </li><!-- --><li> <img src="http://placehold.it/600x360?text=2" /> <a href="">記事のタイトルなどのテキスト要素なので、画面の幅によって改行が生じます</a> </li><!-- --><li> <img src="http://placehold.it/600x360?text=3" /> <a href="">記事のタイトルなどのテキスト要素</a> </li><!-- --><li> ... </ul> |
CSSは.block-grid li
に指定していたfloat: left;
をdisplay: inline-block;
に入れ替えて、vertical-align: top;
を追加しました。
1 2 3 4 5 6 |
.block-grid li { display: inline-block; vertical-align: top; margin: 0 1% 30px; padding: 0; } |
inline-blockの隙間を無くす2つの方法
上でコメントアウトで消したli
要素間のスペースは、親要素のfont-size
やletter-spacing
を使って消すことも可能です。
font-sizeを使う場合
以下のように親要素にfont-size: 0;
を入れてli
要素にfont-size: 16px;
を入れることでli
要素間の隙間をなくすことができます。この際、font-size
はem
や%
の相対値ではli
内の文字が表示されなくなるため、px
などの絶対値を記述する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.block-grid { width: auto; overflow: hidden; list-style: none; margin: 0 -1% 30px; padding: 0; font-size: 0; }. block-grid li { display: inline-block; vertical-align: top; margin: 0 1% 30px; padding: 0; font-size: 16px; } |
letter-spacingを使う場合
以下のように親要素にletter-spacing: -0.4em;
を入れてli
要素にletter-spacing: normal;
を入れることでli
要素間の隙間をなくすことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.block-grid { width: auto; overflow: hidden; list-style: none; margin: 0 -1% 30px; padding: 0; letter-spacing: -0.4em; } .block-grid li { display: inline-block; vertical-align: top; margin: 0 1% 30px; padding: 0; letter-spacing: normal; } |
Flexboxならもっと簡単?
Flexbox版も試しに作ってみました。自動でブロックの高さが同じになるので、各ブロックに背景やボーダーを入れたい場合などはFlexboxを使うとめちゃくちゃ楽できそうです。IEのバージョンを気にしなくてすむならFlexbox版が一番いいかもしれませんね。
CSS
親要素になる.block-grid
にdisplay: flex;
とflex-wrap: wrap;
を追加してul
要素をFlexboxで表示するように指定します。また、子要素の.block-grid li
にflex: 0 1 98%;
を追加して、子要素の表示方法を指定します。この部分をメディア・クエリで画面幅に合わせて変えていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
.block-grid { display: flex; flex-wrap: wrap; list-style: none; margin: 0 -1% 30px; padding: 0; } .block-grid li { flex: 0 1 98%; margin: 0 1% 30px; padding: 0; background: pink; } .block-grid a { text-decoration: none; font-size: 1.6em; } @media (min-width: 600px){ .block-grid-1-2-3 li { flex: 0 1 48%; } } @media (min-width: 800px){ .block-grid-1-2-3 li { flex: 0 1 31.3333%; } } |