🏁

`grid`だけで子コンポーネントを高さいっぱいにする

に公開

結論

ページを高さいっぱいにしたいときは layout で <div className="grow grid">{children}</div> と書くと良い。

コンポーネントの高さを広げる方法

一般的に知られているように CSS において、ブロック要素はデフォルトで親コンテナの幅いっぱいになります。しかし高さはそうではなく、コンテンツの高さになります。

コンテンツを縦に並べていくだけであればそれでも困らないのですが、親コンテナ側で決まった高さがありそれにコンテンツの高さを合わせたいときが存在します。代表的な例はコンテンツの最小の高さを画面に合わせてフッターをページ下に固定したいときです。

そのようなとき子要素の高さを広げる方法としてコンテナに flex flex-col コンテンツに grow を指定する方法がよく使われます。コンテンツ領域を広げてフッターをページ下に固定するときはこのようになるでしょう。

export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body className="flex flex-col min-h-screen">
        <Header />
        <div className="grow">{children}</div>
        <Footer />
      </body>
    </html>
  )
}

しかしときにはこれだけでは不十分な場合があります。各ページに書かれる実際のコンテンツの高さが変わらないのです。あくまで grow したのは layout にある div であり、その {children} 配下には全く影響しません。ページ側に <div class="bg-gray-400">content</div> と書くことで内容の一行分しか背景が設定されないことを確認できます。

複数の要素があるコンテナ内で、要素を選択して広げたいときに flex はとても便利ですが、子コンポーネントに対しても使おうとするとカプセル化の問題が発生します。つまり同じ方法で layout に <div className="flex"> 子コンポーネント側に <div className="grow"> と書くことで、それらが相互の依存を発生させます。

コンポーネントは margin を持つべきではないと言われるのと同じように、レイアウトに関する指定は親側が決めるべきことで、子コンポーネント側はその領域に合わせて表示されることが理想形といえるでしょう。

コンテンツのサイズを最大化する grid

そんなときにはコンテナに grid を指定するだけで解決します。通常 grid はレイアウトを指定してコンテンツを配置するときに使われますが、デフォルトで自身のサイズに合わせてコンテンツを配置するのです。

 export default function RootLayout({ children }) {
   return (
     <html lang="ja">
       <body className="flex flex-col min-h-screen">
         <Header />
-        <div className="grow flex">{children}</div>
+        <div className="grow grid">{children}</div>
         <Footer />
       </body>
     </html>
   )
 }
 export default function Page() {
   return (
-    <div className="grow">
+    <div>
       <Content />
     </div>
   )
 }

おまけ:grow を使わずに要素をページ下に固定するテクニック

ページコンテンツを高さいっぱいにしたり、フッターをページ下に固定するには前述した方法で十分ですが、ときには grow が必要ではないときもあります。そんなときに flex を追加せずに要素をページ下にテクニックを紹介します。

フッター固定のためにメインコンテンツを flex-grow: 1; で広げてフッターを押し出す代わりに、フッター自身を position: sticky; で固定する方法です。一見すると sticky なので画面下に張り付きそうですが、しっかりページコンテンツの下に表示できます。

この方法では position: sticky; top: 100%; を使います。ただしそのコンテナは min-height: 100vh; 相当である必要があります。結局コンテナの高さを設定する必要があるので単体で使える理想的な方法ではないですが、 flex, grow を省くことができるため、コンテナの高さだけを考えればよくなりシンプルになると思います。

ここでは RootLayout では共通のヘッダーを表示するが、フッターは各ページによって出しわけることを考えます。まずは RootLayout で前述した gird を使う方法でページコンテンツを高さいっぱいにします。

export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body className="flex flex-col min-h-screen">
        <Header />
        <div className="grow grid">{children}</div>
      </body>
    </html>
  )
}

そしてページ側ではフッターを内包するコンテナに sticky top-full を指定することでフッターを固定します。

export default function Page({ children }) {
  return (
    <div>
      <Content />
      <div className="sticky top-full">
        <Footer />
      </div>
    </div>
  )
}

この方法は sticky の動作を理解するものとしても面白い例です。これらの動作が組み合わさって機能しています。

  • sticky 要素は通常の要素のように高さを確保すること
  • sticky 要素はコンテナの中でしか固定されないこと
  • top-full によってそのコンテナの一番下まで押し出していること

つまり sticky top-full を指定した要素は、スクロール領域に対してそのコンテナの高さまで押し出そうとするが、実際にはコンテナ内でしか固定されないためコンテナ下部に張り付くという動作をします。

まとめ

  • layout では <div className="grow grid">{children}</div> と書くと良い
  • grow を使うまでもないところでフッターを固定したいときは sticky top-full を使うと良い

Discussion