🐥

プログラムによるレイアウト制御のための CSS Grid を考える

2021/10/14に公開

この記事は、既存のCSSのレイアウトの文脈ではなく、「プログラムから制御されるレイアウト」をいかに綺麗に制御・生成するか、です。

複雑なSPAや何らかのオーサリング環境で、主に JavaScript の視点からレイアウトを扱うのに Grid をどう活かしていくか、という話。

grid-template-areas の視覚的な対応

IEがない世界では CSS grid のフル機能を使うことができます。

自分が grid を使う際、今まで grid-template-areas を気に入って使っていました。これは CSS の視覚的な情報が最終的な表示と一致する、という理由からです。

例えば、 svelte で書いた grid-template-areas を使ったレイアウト設定のコードはこんな感じになります。。

Layout.svelte
<div class="grid">  
  <div style="grid-area: text1">
    <slot name="text1" />
  </div>
  <div style="grid-area: text2">
    <slot name="text2" />
  </div>
  <div style="grid-area: img1">
    <slot name="img1" />
  </div>
</div>

<style>
.grid {
  width: 500px;
  height: 500px;
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(12, 1fr);
  grid-template-areas:
    ".     text1 text1 .     .     .     .     .    "
    ".     text2 .     img1  img1  .     .     .    "
    ".     .     .     img1  img1  .     .     .    "
    ".     .     .     img1  img1  .     .     .    "
    ".     .     .     img1  img1  .     .     .    "
    ".     .     .     img1  img1  .     .     .    "
    ".     .     .     img1  img1  .     .     .    "
    ".     .     .     .     .     .     .     .    "
    ".     .     .     .     .     .     .     .    "
    ".     .     .     .     .     .     .     .    "
    ".     .     .     .     .     .     .     .    "
    ".     .     .     .     .     .     .     .    ";
}
</style>

利用側

<script>
  import Layout from "./Layout.svelte";
</script>
<Layout>
  <div slot="text1">xxx</div>
  <div slot="text2">yyy</div>
  <img src="..." slot="img1" />
</Layout>

grid-template-areas はある種のアスキーアートみたいなもので、スペース区切りで grid-area の名前を入力します。. は空いたセルの意味です。

これは svelte の例ですが slot のセマンティクスは webcomponents や vue と一緒なので、簡単に理解できるでしょう。対応する slot のところにコンテンツがはめ込まれます。

実際の1セルの幅は上の例だと repeat(8, 1fr) で、fr (flex でいう flex: n の比率) という単位で8等分していますが、16px 1fr 15px 2rem 3fr ... と、細かい分割幅を指定することもできます。

grid-template-areas の発想としては、二次元マトリクス上でエリアを命名して、利用側からは実際に配置される場所を知らないまま名前の対応で配置する、という手順です。これにより、利用側で知るべきは、期待されてる slot 名だけ、ということになるわけです。

grid-template-areas - CSS: カスケーディングスタイルシート | MDN

flex と比較すると、二次元上の配置をするときに 別の div を挟んで flex-direction を切り替える必要がありません。これが grid の flex に対する明確に有利な点だと思います。

grid-template-areas の限界: overlap

自分が知る限り grid-template-areas は2つの問題がありました。

  1. 分割単位のセルを細かくするにつれて、CSS の出力サイズが増える
  2. セルを交差する grid-area が表現できない

1 はパフォーマンス上の問題で、しかも 3rd party script ぐらいの小さい粒度のときだけ問題になるんですが(自分は svelte で3rd party script を作っていて問題になった)、2 は表現上の問題で、これが求められたときに詰んでしまいました。grid-template-areas はセルが一つの area に専有されている前提なので、複数のセルにまたがったときに破綻します。

どういうことかというと、背景に画像を敷いて、その上にテキストを置きたい、みたいな表現ができません。

テキスト要素の背景に画像を設定することは可能でしょうが、大きな画像の上にテキストを複数置く、がやはり不可能です。

この表現をどうやるかと調べたら Overlapping Grid Items
という記事を見つけて、 grid-area を子要素の側から指定すればいいとのこと。

.item.one {
  grid-row-start: 1;
  grid-row-end: 2;
  grid-column-start: 1;
  grid-column-end: 4;
}

これの省略形が grid-area で 一気に4つ指定できます。

.item.two {
  grid-area: 1 / 1 / 4 / 2;
}

じゃあこれを css variables で指定できればよくね?となり、やってみることにしました。

CSS Vars と Grid を組み合わせたレイアウト指定

この grid-area を使って、親の側からレイアウト用の変数を指定します。grid-row-start や grid-column-end だと自分には分かりづらかったので、 x1,y1,x2,y2 の矩形で考えることにしました。

生のHTMLとCSSです。

<div class="grid">
  <div class="grid-item" style="--x1: 5; --y1: 2;--x2: 8; --y2: 7; --color: red; --z: 1;"></div>
  <div class="grid-item" style="--x1: 2; --y1: 4;--x2: 11; --y2: 5; --color: white; --z: 2"></div>
</div>
.grid {
  width: 400px;
  height: 400px;
  grid-gap: 10px;
  background-color: wheat;
  display: grid;
  grid-template-rows: repeat(16, 1fr);
  grid-template-columns: repeat(16, 1fr);
}

.grid-item {
  grid-area: var(--y1) / var(--x1) / var(--y2) / var(--x2);
  background: var(--color);
  z-index: var(--z);
}

このとき、x1,y1,x2,y2 が 0 オリジン…ではなく 1 オリジンです。--x1: 0; --y1: 0; は左上ではなく、無効な grid-area 指定になります。

なので、0 オリジンにしたい場合は calc も使って次のようになるでしょう。

.grid-item {
  /* y1, x1, y2, x2 */
  grid-area: calc(var(--y1) + 1) / calc(var(--x1) + 1) / calc(var(--y2) + 1) / calc(var(--x2) + 1);
  background: var(--color);
  z-index: var(--z);
}

というのを codepen に置いておきます。

https://codepen.io/mizchi/pen/bGrdrdo

Svelte での Grid 要素

↑のコードをそのまま使うと、style ブロックのインラインでの記述はあんまりエディタの支援とかが得られなくて残念な感じになりそうだったので、フレームワークごとの表現に落とすと扱いやすそうな気がしました。

今回は Svelte でやってみます。 Grid と GridItem という2つのコンポーネントを定義します。

<!-- Grid.svelte -->
<script>
  export let rows;
  export let columns;
  export let gap = "8px";
  export let background = "transparent";
  $: gridRoot = `
grid-template-columns: ${typeof columns === "number" ? `repeat(${columns}, 1fr)` : columns };
grid-template-rows: ${typeof rows === "number" ? `repeat(${rows}, 1fr)` : rows };
grid-gap: ${gap};
background: ${background};`;
</script>

<div class="grid" style={gridRoot}>
  <slot />
</div>

<style>
  .grid {
  width: 500px;
  height: 500px;
  display: grid;
}
</style>
<!-- GridItem.svelte -->
<script>
  export let x1;
  export let x2;
  export let y1;
  export let y2;
  export let background = 'black';
  $: gridAreaStyle = `grid-area: ${y1 + 1} / ${x1 + 1} / ${y2 + 1} / ${x2 + 1}; background: ${background}`;
</script>
<div style={gridAreaStyle}>
  <slot />
</div>

で、これを使うコードはこんな感じになります。

<script>
  import Grid from "./Grid.svelte";
  import GridItem from "./GridItem.svelte";
</script>

<Grid
  rows={16}
  columns={16}
  gap="8px"
  background="wheat"
>
  <GridItem x1={3} y1={3} x2={9} y2={5} background="rgba(255,0,0,0.5)">
    xxx
  </GridItem>
  <GridItem x1={5} y1={1} x2={6} y2={7} background="rgba(255,0,0,0.5)">
    yyy
  </GridItem>
</Grid>

grid-template-areas と比べて視覚的な直感は得られないですが、代わりに明示的な座標指定で x,y の2次元マトリクスで考えられるようになります。

grid-template-aresas で可能だったレイアウトと実際の位置の分離はできないですが、一層レイアウト用のコンポーネントでラップすれば可能ではあり、視覚的なコントロールをしやすいので一長一短な気がします。

というのを svelte playground に置いてます

https://svelte.dev/repl/212486073e66425cab49b8dc1af70a85?version=3.43.1

おまけ: 他の grid の用途

一番簡単な中央寄せの実装。

.center {
  width: 100%;
  height: 100%;
  display: grid;
  place-items: center;
}

flex でもできますが、こっちの方が楽です。

おまけ: grid-template-areas と media query

grid-template-areas を media query で切り替えるテクニック。レスポンシブ対応で同じコンテンツを吐き出し直すのが簡単です。

.grid {
  display: grid;
  width: 500px;
  height: 500px;
  grid-template-rows: 1fr 1fr 1fr;
  grid-template-columns: 1fr 1fr 1fr; 
}
.grid-item {
  display: grid;
  place-items: center;
  background: wheat;
}

.grid {
  grid-template-areas:
    'a b c'
    'a b c'
    'a b c'
}
/* モバイルのとき */
@media (max-width: 480px) {
  .grid {
  grid-template-areas:
    'a a a'
    'b b b'
    'c c c'      
  }
}

Grid の不利な点

  • IE で動かないわけではないですが、古い仕様なので、Autoprefixer 等で修正する必要があります
  • React Native で使えません。grid で学んだことをあまり活かせないので、 CSS Grid を学ぶほど残念な気持ちになるかもしれません。
    • IEサポートが必要な今のCSSの主流が flex な気がしており、そうだとすると CSS Grid を読める人が少ないかもしれません
  • 可変なコンテンツを入れ込むのに向いていません。エリアから溢れた分ははみ出します。
  • 一方向に並べるときの grid は flex と比べてやりすぎ感があります。

おわり

grid について学びたい人は grid garden がおすすめ。

Grid Garden - CSS grid が学べるゲーム

この記事に書いたいくつかのテクニック、とくに overlap の css vars で扱う方法は Grid-based website builder | Grid.studio というサービスが吐き出すCSSを観察することで得た知識です。

grid はプログラマブルな表現がしやすいので、レイアウト構造をデータ構造と対応させて可変に制御するのに向いてそうで、色々なことができそう、みたいな話でした。

Discussion