BeAct Co., Ltd.

BLOG
社員ブログ

グリッドレイアウトでカードを並べよう!

こんにちは! みなさん、今日もコーディングを楽しんでいますか?

こんかいのお題は、便利そうだけど、わかりそうでわからない。調べるほどに頭がこんがらがる……と(主にわたしが)思っている、CSSの「グリッドレイアウト」についてです。グリッドレイアウトは優れた仕組みではあるのですが、その実現のためには考えるべきことが多すぎる……わたしと同様に、そう思っている方も多いのではないでしょうか。

個人的に、グリッドレイアウトを使う目的は以下の2つだと考えています。

  1. 新聞や雑誌のような、不規則なレイアウトを実現する。
  2. 個数が増減するコンテンツを、高さや幅を揃えつつ、カードを並べるように整列させる。

こんかいは、このうちの「2」、すなわち「カードレイアウト」を題材に、一緒に学んでゆきましょう。カードレイアウトは使う機会も増えてきていますので、学習の第一歩としても妥当なのではないでしょうか。

注意)

こんかいの記事は、正確さよりも、理解のしやすさを優先しています。なので、「これを読めばグリッドレイアウトはばっちり!」ではなく、「カードレイアウトに必要な部分だけ、知識をつまみ食いする」くらいの気持ちで読んでいただければ幸いです。

なお、例示しているコードに記載されている企業名や内容は、全て架空のものです。


カードレイアウトとは?

ショッピングサイトなどで、このようなレイアウトをよく見かけると思います。項目ごとにカードを整列させたように配置されていますが、見出しや内容の高さが、でこぼこせずに揃っているのが特徴です。

これが「カードレイアウト」です。CSSのグリッドレイアウトの登場で、このような表現は多く見られるようになりました。こんかいお勉強するのは、こちらの実現方法です。

グリッドレイアウトの基本

さて、本題に入る前に、カードレイアウトを作るために必要なグリッドレイアウトの用語を、おさらいしたいと思います。

グリッドコンテナ

直接の子要素(グリッドアイテム)にグリッドレイアウトのルールを適用させる、いわば親要素です。グリッドレイアウトの指定の大半は、このコンテナ側で行われます。そのため、その項目は数多くあります。

通常のグリッドレイアウトでは、ここでグリッドアイテムの幅と高さの指定を行います。しかし、一般的なカードレイアウトに限っては、アイテムの幅の指定はしても、高さの指定はしません。これは後述するサブグリッドとの関わりによるものです。

グリッドアイテム

グリッドコンテナの直接の子要素で、コンテナから指定されたルールに従って配置されます。もちろんグリッドアイテム自身にも、配置のための指定項目があります。

グリッドトラック

グリッドコンテナがグリッドアイテムを配置するために設けた、仮想的な領域です。目に見えないタテヨコの罫線で区切られた、一つ一つのマス目と考えればよいでしょう。グリッドアイテムはこの中に配置されます。

特に宣言のない限り、グリッドアイテムはグリッドトラックいっぱいに引き伸ばされて配置されるため、(特にカードレイアウトにおいては)グリッドアイテム=グリッドトラックと考えても問題はありません。ただし、このグリッドトラックの概念を知らないと、思った通りの表示ができないときの原因調査に難航することがあります。

サブグリッド

カードレイアウトの理解において、一つの山場となる考え方です。

グリッドアイテム自身も{display: grid;}を指定することで、グリッドコンテナとなることができます。これは単なる「グリッドコンテナの入れ子」であり、親のグリッドコンテナとは無関係に働きます。

一方でサブグリッドは、「親のグリッドコンテナの行の定義を、自分の行の定義とするグリッドコンテナ」です。 ……はて、どういうことでしょうか? ここが大事なところですが、

「高さがでこぼこしていないこと」とは、親が引いた罫線に従って、自身の要素を配置すること

だからです。いうなればサブグリッドとは、親の行の定義に依存したレイアウトなのです。これはとても大切な考え方なので、忘れないようにしましょう!

{grid-template-rows: subgrid;}を宣言することで、そのグリッドアイテムはサブグリッドとなります。

グリッドコンテナ側の宣言

それでは、このレイアウトはどのように作られているのでしょうか? まずはグリッドコンテナの定義について、上の例を簡略化したコードで説明してみましょう。

/** グリッドコンテナ */
ul.grid-container {
  // これによって、グリッドレイアウトを宣言する
  display: grid;
  // グリッドトラックの幅は 12rem であり、はみ出したら折り返す
  grid-template-columns: repeat(auto-fill, 12rem);
  // グリッドトラック同士は、 1rem のすきまを空けて並べる
  gap: 1rem;
  // 行がグリッドトラックで埋まったとき、コンテナの中で中央に寄せて表示する
  justify-content: center;
}

display: gridは説明不要だと思いますので、次の行から説明しますね。


grid-template-columns

「このグリッドコンテナは一行につき、どれくらいの幅のグリッドトラックを、最大いくつ並べられるか」の宣言です。

指定する値

(こんかいは「カードレイアウトの実現」に絞って話をしますので、より一般的な指定方法は割愛させていただきます。)

repeat( auto-fill または auto-fit, グリッドトラックの幅)という形式で定義します。これは、

カードの幅はグリッドトラックの幅。コンテナをはみ出さない限り左から右に並べて、はみ出す場合は折り返す。

という意味になります。

またauto-fillauto-fitの違いは、

  • auto-fill:
    • グリッドトラックの幅の制限内で、一行に詰められるだけ詰める。余ったら折り返し、足りなければ余白部分は「暗黙のグリッド」となる。
  • auto-fit:
    • グリッドトラックの幅の「成り行き」が許される場合、上記「暗黙のグリッド」の幅をトラック同士で分け合って、トラック自身の幅を広げる。結果、「暗黙のグリッド」は生まれない。

です。上の例では「グリッドトラックの幅は固定(成り行きを許さない)」なので、どちらを選んでも同じ結果となります。


gap

グリッドトラック同士にすきまを空けたい場合、「どのくらい空けるか」の宣言です。レイアウト業界によってガター (gutter) やアレー (alley) などと呼ばれるもので、CSSではギャップ (gap) と呼びます。

指定する値

CSSの長さ単位(rem, em, px…)で指定します。


justify-content

「グリッドコンテナの中にトラックを横に並べるとき、左右に空いたすきまをどのように扱うのか」の宣言です。これは以下の「指定する値」の画像を見ていただいたほうが早いでしょう(違いがわかりやすいように、グリッドトラックをわざと小さくしています)。

ちなみに、画像のピンク色の線のうち、縦線同士で挟まれた領域が、グリッドトラックです。

指定する値

以下の値が多く使われます。

  • start/normal(デフォルト)
    • gap の値を保ちながら、グリッドトラックの置き始めの方向(ここでは左)に寄せます。
  • end
    • gap の値を保ちながら、グリッドトラックを置いてゆく方向(ここでは右)に寄せます。
  • center
    • gap の値を保ちながら、グリッドコンテナの中央に寄せます。
  • space-between
    • gap の値を無視して、グリッドトラックをグリッドコンテナの幅いっぱいに並べます。
  • space-around
    • gap の値を無視して、グリッドトラックの両端に同じ長さのスペースがあるかのように並べます。
  • space-evenly
    • gap の値を無視して、グリッドトラック同士が同じ隙間になるように並べます。

上記の配置例は、以下のライブコードから試すことが出来ます。ラジオボタンを切り替えて、配置の違いを実感してみましょう!


justify-items

後述するグリッドアイテム側の宣言justify-selfを、グリッドコンテナでまとめて行なうための宣言です。

詳細は、該当の項目で述べるようにします。

カードの幅を成り行きで伸び縮みさせるには?

冒頭ではカードの幅を12remに固定した例を挙げましたが、いわゆるレスポンシブ的に「カードの最低の幅だけ決めて、いい感じに伸び縮み」……という「成り行き」のレイアウトもよく見かけますよね。その場合は、

repeat(auto-fit, minmax(最低のグリッドトラックの幅, 1fr))

で、実現できます!

frってなに?

ここで出てきたfrという単位。これはフレックス係数と呼ばれる単位です。justify-content のコード例で、「すきまをどのように扱うか」と述べましたが、この単位は余ったすきま(「暗黙のグリッド」)に対し「ぼくは1つぶん」「おいらは2つぶん」「わたしは3つぶん」と、各々が欲しいぶんを分け合った結果を表します。定量ではなく、可変量の単位ですね。

minmax(最低のグリッドトラックの幅, 1fr)で記述された場合は、

最低のトラック幅の場合に出来るであろうすきま=暗黙のグリッドを埋めるように、グリッドトラックの幅を均等に足し合わせる=カードの幅を成り行きで伸ばす

という意味になります。

グリッドアイテム側の宣言

グリッドレイアウトの大部分はグリッドコンテナ側の宣言で決まりますが、もちろんグリッドアイテム側にも重要な宣言があります。最初の例のように、簡略化したコードで説明していきましょう。

/** グリッドアイテム */
.grid-item {
  justify-self: strech;
}

justify-self

グリッドコンテナ側で軽く触れたjustify-itemsを、グリッドアイテム自身が行う宣言です。

この宣言は、「グリッドトラックの中で、グリッドアイテムをどう配置するか」を規定します。……はて、どういうことでしょうか? 論より証拠。こちらもまた、以下の「指定する値」の例を見ていただくことにしましょう。

なお、図中のピンク色の縦線で挟まれた領域がグリッドトラック、実際のカード要素がグリッドアイテムです。グリッドトラックとグリッドアイテムとの関係が、これでわかると思います。

指定する値

以下の4つの値が使われます。

  • strech(デフォルト)
    • グリッドアイテムを、グリッドトラックいっぱいに引き伸ばして(ストレッチ=伸ばす)描画します。アイテムの幅が決められていればその値に固定されるため、カードレイアウトでは一般的にこれが使われます
  • start
    • グリッドアイテムは、グリッドトラック内で、グリッドトラックの置き始めの方向(ここでは左)に詰められます。
  • end
    • グリッドアイテムは、グリッドトラック内で、グリッドトラックを置いてゆく方向(ここでは右)に詰められます。
  • center
    • グリッドアイテムは、グリッドトラック内で、中央寄せされます。

上の例でご理解いただけたと思いますが、グリッドトラックとは、

そのグリッドアイテムで{justify-self: strech;}が指定されたとしたら、占められるであろう領域(図の中のピンク色の縦線=gapで区切られた範囲)

のことなのです。

上記の配置例は、以下のライブコードから試すことが出来ます。グリッドコンテナのjustify-contentと合わせて、ラジオボタンを切り替えて、配置の違いを実感してみましょう!

……とはいえ、カードレイアウトにおいて「特定のカードだけ、justify-selfで、トラック内での寄せ方を変えよう」というケースはまずありませんので、グリッドコンテナ側から一律にjustify-itemsで指定するか、いっそデフォルト値である{justify-items: strech;}にまかせてしまってもいいと思います。

グリッドアイテムからサブグリッドにしたときの宣言

いよいよここからが山場です。いっしょに頑張って学習していきましょう。

まずは HTML のコードをご確認ください。

<!-- サブグリッドとなるグリッドアイテム -->
<article class="grid-item">

  <!-- 子要素: 1 -->
  <img src="https://picsum.photos/175/130/" height="130" alt="" />

  <!-- 子要素: 2 -->
  <header>
    <h2>札幌温泉めぐり 2泊3日</h2>
  </header>

  <!-- 子要素: 3 -->
  <dl>
    <div>
      <dt>料金</dt>
      <dd>¥24,000(税別、お一人様)</dd>
    </div>
    <div>
      <dt>最低催行人数</dt>
      <dd>10名様</dd>
    </div>
  </dl>

  <!-- 子要素: 4 -->
  <address>
    <p>ビーアクト観光</p>
    <p>TEL: xxx-xxxx-xxxx</p>
  </address>

</article>

HTML のマークアップを見ると、<article>タグの直接の子要素が4つありますね。そしてその各要素は、先ほどからご覧いただいているカードの「高さをそろえる」部分であることにご注目ください。

サブグリッドはグリッドアイテムでありながら、グリッドコンテナでもあります。ここで、グリッドアイテムとは、グリッドコンテナの直接の子要素であるというルールを思い出してください。つまり、この4つの要素は、サブグリッドにとってのグリッドアイテムということになります。

つぎに、ここに当てる SCSS のコードをご確認ください。

/** グリッドアイテムからサブグリッドにする */
.grid-item {
  // まずはグリッドコンテナだと宣言
  display: grid;
  // 「この `グリッドコンテナ` の行の定義は、親グリッドコンテナに依存する=サブグリッドである」という宣言
  grid-template-rows: subgrid;
  // 「この `グリッドアイテム` は、親グリッドの4行にまたがる高さである」という宣言
  grid-row: span 4;
  // 「親の `gap` を流用せず、自身で `gap` を定義する」という宣言
  row-gap: 0.5rem;
}

それでは、今の説明を踏まえたうえで、スタイルシートの説明に入ってゆきます。途中、よくわからない説明があるかもしれませんが、後でライブコードや画像などで説明しますので、まずは一通り目を通してくださいね。


display: grid;

項目名と値は決まっています。

この宣言と値は、この要素はグリッドコンテナであるという意味です。グリッドアイテムをサブグリッドに変えるためには、各グリッドアイテムに対してまずこの宣言が必要です。

なお、この宣言によって、この要素はグリッドアイテムであると同時に、グリッドコンテナにもなります。よって以降の定義は、「グリッドアイテムとしての宣言」「グリッドコンテナとしての宣言」が混ざるようになりますので、混乱しないように気をつけてください。


grid-template-rows: subgrid;

グリッドコンテナとしての定義です。項目名と値は決まっています。

この宣言と値は、このグリッドコンテナの行の定義は、親のグリッドの行の定義に依存する。すなわちサブグリッドであるという意味です。すでに述べましたが、{display: grid;}にて「この要素はグリッドコンテナである」と宣言されていなければなりません。


grid-row: span 行数;

グリッドアイテムとしての定義です。項目名と値は決まっています(行数には数字が入ります)。

この宣言と値は、自身のアイテム領域の高さは、親のグリッドコンテナの行を「行数」ぶん結合させて生成するという意味です。

spanとは、 HTML の<table>タグ内部に記述する<td rowspan="結合する行数">をイメージしていただけると良いでしょう。コード例の{grid-row: span 4;}は「4行ぶん結合する」という意味になります。この4という数字は、グリッドアイテムのHTMLの説明で触れた「サブグリッドとなったグリッドアイテムの、直接の子要素の数」であり、この4要素をつなげて1枚のカードを表現する、と考えるとわかりやすいでしょう


row-gap

グリッドコンテナとしての定義です。

この宣言は、自身のコンテナに置かれるアイテム同士の縦方向のすきまを、どのくらい空けるのかという意味です。通常のグリッドコンテナでもgapが使われますが、それの縦方向のみのバージョンと考えてよいでしょう。それでもわざわざここで取り上げた理由は、サブグリッドでは少し意味が変わるからです。

サブグリッドのグリッドコンテナでのgapは、「親のgapに依存しない、新たなgapを定義する」という意味になります。例えばカードレイアウトにおいて、親のgapはカード同士の間隔を意味しますが、そのままだと各カード自身にも親のgap値が適用されてしまい、カード内に同じ幅のすきまができてしまいます。カードのデザインによっては、そのような見せ方をしたくないこともあるでしょう。それに対処するため、サブグリッドのグリッドコンテナに限り、「親のgap値に依存しない、新しいgap値」という意味になっています。

指定する値

CSSの長さ単位(rem, em, px…)で指定します。

実際に見てみよう!

サブグリッドについてまとめて説明しましたが、文章だけではなかなかイメージしづらいと思います。そこで、こちらもまたライブコードを用意いたしましたので、文章を読みながらチェックボックスやラジオボタンを切り替えてみてください。

「親の行に依存」「行数ぶん結合」という言葉の理解の助けになれば幸いです。

それぞれの見え方の違いについて、説明していきますね。

  • (成功)サブグリッド、行数ぶん結合、gapは親まかせ
    • ピンクの線の、こんどは横方向(行)に注目してください。この線で区切られた区画の数は「4」です。{grod-row: span 4}によって4行分を結合するとは、グリッドアイテム(サブグリッド)のグリッドアイテムの個数である「4」つの区画を使って、1枚のカードを表現するという意味です。
      各カードの表示要素が、ピンクの線に沿って並んでいることにもご注目ください。これが、「親の引いた罫線に従ったレイアウト」の意味だったわけですね。
  • (大成功!)サブグリッド、行数ぶん結合、gapはアイテムが宣言
    • 最初の例では、親が決めたカード同士のgapを、カードの内容のgapとして流用しているので、スカスカな見た目になっていますね。サブグリッドでのgapは、これを調節するためのものです。
      左から2番めの、黄色の線にご注目ください。最初の例ではこの線はピンクの線と同じ太さでしたが、サブグリッドで改めてgapを宣言すると、黄色の線はその値になります。つまり、親のレイアウトに沿いながら、自分自身のgapで表示することができるのです! これこそ求めていた見た目の、カードレイアウトですね!
  • (失敗例)単なるグリッドコンテナの入れ子、gap宣言はしない
    • カードに{grid-template-rows: subgrid;}指定を忘れてしまった例です。ピンク色の線が、カードにまたがってないですね。つまり、サブグリッドになっていない、ただの入れ子のグリッドコンテナとなってしまっているわけです。
      これでは親の行のレイアウトを参照できず、めいめいのカードが勝手なレイアウトを使ってしまいます。これが「上下にでこぼこ」してしまう原因のひとつです。
  • (失敗例)単なるグリッドコンテナの入れ子、gap宣言はする
    • これも同様です。違いは入れ子の内側にrow-gap指定をしていることだけです。黄色い線で隙間を示していますが、もちろんサブグリッドではないので、単なる「グリッドコンテナによる間隔の宣言」になってしまっていることがわかると思います。
  • (失敗例)「行の結合」を忘れた
    • せっかくサブグリッド化に成功しても、{grid-row: span 4;}による「行の結合」を忘れると、サブグリッドのグリッドアイテムがすべて「親の1行の中」に押し込まれてしまいます!
      最初はびっくりするかもしれませんが、行結合を忘れただけですので、落ち着いて対処すれば大丈夫です!

以上からわかる、カードレイアウトの基本は:

  • カードをサブグリッド宣言する。
  • カードを並べたときに高さを揃えたい要素を「カードの直接の子要素」にする。
  • カードの子要素同士の間隔は、カード自身のgapで宣言する。
  • 子要素の数だけ、行の結合を宣言する。
  • 親のグリッドコンテナで、カードの幅と、カード同士の間隔(gap)を宣言する。

ということになります。さあ、みなさんも一緒にやってみましょう!

「直接の子要素」じゃない場合はどうする?

……と言いたいところですが、実はこんかいのライブコードには、説明と矛盾するところがありました。

<ul class="grid-container"><!-- グリッドコンテナ -->
  <li><!-- 直接の子要素は、これ -->

    <article class="grid-item"><!-- これは直接の子要素じゃない! -->
      <img src="https://picsum.photos/175/130/" height="130" alt="" />
      <header>
        <h2>札幌温泉めぐり 2泊3日</h2>
      </header>
      <dl>
        <div>
          <dt>料金</dt>
          <dd>¥24,000(税別、お一人様)</dd>
        </div>
        <div>
          <dt>最低催行人数</dt>
          <dd>10名様</dd>
        </div>
      </dl>
      <address>
        <p>ビーアクト観光</p>
        <p>TEL: xxx-xxxx-xxxx</p>
      </address>
    </article>

  </li>

  <!-- 省略 -->

</ul>

グリッドアイテムになれるのは、グリッドコンテナの直接の子要素のみというルールだったはずです。でも、この HTML は、そのルールに従っていません!

実は HTML の規則として、<ul><ol>の直接の子要素は、<li>以外認められていません。でもセマンティクス的には、カードを「ひとつの話題のかたまり」と解釈して欲しいので、なんとしても<article>タグで囲いたいところです
<li role="article">にすればいいという話は、今はおいておきましょう)。

こうした HTML(マークアップ)と CSS(スタイリング)とで定義がミスマッチを起こすことは、想像以上に発生しがちです。そのため CSS には、「親の子要素というものを、ぼくじゃなくて、ぼくの子要素に読み替えてスタイルを当ててください」という宣言があります。仕様を策定した人たちの苦労が偲ばれますね。

li {
  // これが「ぼくの直接の親の子要素は、ぼくではなく、ぼくの直接の子要素に読み替えて」という宣言
  display: contents;
}

{display: contents;}を宣言した要素はあら不思議、CSS上では「ない」ものとみなされます! さながらこんなふうに。

<ul class="grid-container"><!-- グリッドコンテナ -->
  <!-- <li> ぼくはいないよ… -->

    <article class="grid-item"><!-- 直接の子要素は、これ -->
      <img src="https://picsum.photos/175/130/" height="130" alt="" />
      <header>
        <h2>札幌温泉めぐり 2泊3日</h2>
      </header>
      <dl>
        <div>
          <dt>料金</dt>
          <dd>¥24,000(税別、お一人様)</dd>
        </div>
        <div>
          <dt>最低催行人数</dt>
          <dd>10名様</dd>
        </div>
      </dl>
      <address>
        <p>ビーアクト観光</p>
        <p>TEL: xxx-xxxx-xxxx</p>
      </address>
    </article>

  <!-- </li> ぼくもいないよ… -->

  <!-- 省略 -->

</ul>

これによって、HTML 上でも、CSS 上でも、矛盾のない「直接の子要素」定義ができて、めでたしめでたし、というわけです。

まとめ

……ふう。こんかいの記事はいつもよりも、特に長くなってしまいました。でも、これでもわかりやすさのため、大幅に説明を省略していたりします。グリッドレイアウトは、カードレイアウトを実現するためだけのものではありません。他にも様々な用途で活躍します。

でもそのためには、こんかい紹介した以上の知識を学習することが不可欠……と、今はそれだけお伝えしておきましょう。いずれ、その記事も書きたいと思っていますので、そのときはまたお付き合いくださいね!

こんかいのお話は、以上です。それでは、よきコーディングライフを!