subgridが便利だった話

## はじめに

みなさん、下記のようなコンポーネントをどうやって実装しますか？

- カードが３つ横並び
- タイトルの長さがデータによって異なる
- 説明の長さがデータによって異なる
- タイトル、説明、ポイントラベルそれぞれの上辺を揃える

![](https://storage.googleapis.com/zenn-user-upload/d9b0e4f80ce8-20231125.png)

結構厄介なUIなんですが、全モダンブラウザ対応になったCSSのsubgridを使うととても簡単に実装できたので、その紹介をしたいと思います！

## 対象読者

- subgridに興味がある
- subgridのユースケースを知りたい
- subgridの実装例が見たい

## 動作環境

- node v18.12.1
- vite v4.4.5
- vue v3.3.4

## 前提

今回紹介するソースコードは下記リポジトリで公開しています。

https://github.com/hakushun/dm_subgrid

## 実装例1: 制御しない

まずはコンポーネントを作成していきましょう。

３つのカードはsectionタグで囲って、gridを使って横並びにします。

カードはflexを使って中の要素を縦並びにしました。

```vue
<template>
  <section :class="styles.cards">
    <div v-for="item in items" :key="item.id" :class="styles.card">
      <div>
        <img :src="item.imageSrc" alt="" width="100" height="100" />
      </div>
      <h2>{{ item.title }}</h2>
      <p>{{ item.description }}</p>
      <div>
        <span>現在: {{ item.points.asis }}</span>
        <span>理想: {{ item.points.tobe }}</span>
      </div>
    </div>
  </section>
</template>

<style module="styles">
.cards {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(3, 300px);
  padding: 48px;
  border: 3px solid var(--accent-color);
  border-radius: 18px;
}
.card {
  display: flex;
  flex-direction: column;
  gap: 16px;
  padding: 16px;
  border-radius: 6px;
  background-color: var(--secondary-color);
}
</style>
```

しかし、この実装方法だと説明の上辺の位置とポイントラベルの上辺の位置が３つのカードで揃いません。

![](https://storage.googleapis.com/zenn-user-upload/9a432da1503b-20231125.png)

## 実装例2: JSで制御する

次にJSでDOMの高さを取得して、その高さからマージンを計算し付与する、という実装を考えてみたいと思います。

本題ではないので詳細は説明しませんが、実装としては下記の通りです。

```vue
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from "vue";

const titleRefs = ref<HTMLHeadingElement[]>([]);
const descriptionRefs = ref<HTMLParagraphElement[]>([]);

function addMarginTop() {
  // 3つのタイトルの高さを取得
  const titleHeights = titleRefs.value.map((title) => title.clientHeight);
  // 3つのタイトルの高さで最大の値を取得
  const maxTilteHeight = Math.max(...titleHeights);
  // 説明に付与するマージントップの値を計算
  const marginTops = titleHeights.map((height) => maxTilteHeight - height);
  // 説明に付与するマージントップを付与
  descriptionRefs.value.forEach((element, index) => {
    element.style.marginTop = `${marginTops[index]}px`;
  });
}

onMounted(() => {
  // マウント時に発火
  addMarginTop();
});

watch(items, async () => {
  // itemsを変更を検知し、 DOMの更新処理を待ってから発火
  await nextTick();
  addMarginTop();
});
</script>

<template>
  <section :class="styles.cards">
    <div v-for="item in items" :key="item.id" :class="styles.card">
      <div :class="styles.wrapper">
        <img :src="item.imageSrc" alt="" width="100" height="100" />
      </div>
      <!-- 高さ取得のためrefを付与 -->
      <h2 ref="titleRefs">{{ item.title }}</h2>
      <!-- マージントップ付与のためrefを付与 -->
      <p ref="descriptionRefs">{{ item.description }}</p>
      <div :class="styles.points">
        <span>現在: {{ item.points.asis }}</span>
        <span>理想: {{ item.points.tobe }}</span>
      </div>
    </div>
  </section>
</template>

<style module="styles">
/* 省略 */
.points {
 // 下揃えにするため
  margin-top: auto;
}
</style>
```

挙動はこれで問題ないんですが、これだとカードの中身の要素が増えると計算ロジックを足していくことになり、複雑さが増していきます。

## 実装例3: subgridを使用する

そこでsubgridの登場です！subgridを使うと上記で実装したJSのロジックは全て不要になります！

実装も簡単です。実装例1でcardクラスに当てていたflexをgridに変更し、下記２つのスタイル定義を追加します。

```css
grid-template-rows: subgrid;
grid-row: span 4;
```

grid-template-rowsで行方向に対して`subgrid`を適用することを宣言し、grid-rowで行が4つあることを宣言してます。

今回はカードの中身が画像・タイトル・説明・ポイントラベルの4行だったので`span 4`と指定しましたが、カードの中身に応じて`span 3`や`span 5`に変更してください。

```vue
<template>
  <section :class="styles.cards">
    <div v-for="item in items" :key="item.id" :class="styles.card">
      <div>
        <img :src="item.imageSrc" alt="" width="100" height="100" />
      </div>
      <h2>{{ item.title }}</h2>
      <p>{{ item.description }}</p>
      <div>
        <span>現在: {{ item.points.asis }}</span>
        <span>理想: {{ item.points.tobe }}</span>
      </div>
    </div>
  </section>
</template>

<style module="styles">
.cards {
  display: grid;
  gap: 16px;
  grid-template-columns: repeat(3, 300px);
  padding: 48px;
  border: 3px solid var(--accent-color);
  border-radius: 18px;
}
.card {
  display: grid; // flexからgridに変更
  grid-template-rows: subgrid; // 追加
  grid-row: span 4; // 追加
  padding: 16px;
  border-radius: 6px;
  background-color: var(--secondary-color);
}
</style>
```

これだけでカード同士の横並びが揃うようになりました！

![](https://storage.googleapis.com/zenn-user-upload/d9b0e4f80ce8-20231125.png)

## まとめ

いかがでしたでしょうか？subgridの便利さは伝わりましたか？

CSSをうまく使用することでコード量が減り、可読性や保守性を高めることができました。subgrid以外にもまだまだ使いこなせていないCSSプロパティがあると思うので、引き続き手を動かし学んでいこうと思います。