はじめに
みなさん、下記のようなコンポーネントをどうやって実装しますか?
- カードが3つ横並び
- タイトルの長さがデータによって異なる
- 説明の長さがデータによって異なる
- タイトル、説明、ポイントラベルそれぞれの上辺を揃える
結構厄介なUIなんですが、全モダンブラウザ対応になったCSSのsubgridを使うととても簡単に実装できたので、その紹介をしたいと思います!
対象読者
- subgridに興味がある
- subgridのユースケースを知りたい
- subgridの実装例が見たい
動作環境
- node v18.12.1
- vite v4.4.5
- vue v3.3.4
前提
今回紹介するソースコードは下記リポジトリで公開しています。
実装例1: 制御しない
まずはコンポーネントを作成していきましょう。
3つのカードはsectionタグで囲って、gridを使って横並びにします。
カードはflexを使って中の要素を縦並びにしました。
<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>
しかし、この実装方法だと説明の上辺の位置とポイントラベルの上辺の位置が3つのカードで揃いません。
実装例2: JSで制御する
次にJSでDOMの高さを取得して、その高さからマージンを計算し付与する、という実装を考えてみたいと思います。
本題ではないので詳細は説明しませんが、実装としては下記の通りです。
<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に変更し、下記2つのスタイル定義を追加します。
grid-template-rows: subgrid;
grid-row: span 4;
grid-template-rowsで行方向に対してsubgrid
を適用することを宣言し、grid-rowで行が4つあることを宣言してます。
今回はカードの中身が画像・タイトル・説明・ポイントラベルの4行だったのでspan 4
と指定しましたが、カードの中身に応じてspan 3
やspan 5
に変更してください。
<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>
これだけでカード同士の横並びが揃うようになりました!
まとめ
いかがでしたでしょうか?subgridの便利さは伝わりましたか?
CSSをうまく使用することでコード量が減り、可読性や保守性を高めることができました。subgrid以外にもまだまだ使いこなせていないCSSプロパティがあると思うので、引き続き手を動かし学んでいこうと思います。