【CSS】疑似要素を使ってtbody間に余白を設ける方法

## はじめに

以下のように、`<table>`内で`<tbody>`をループして動的に生成するコードがあります。
データの切れ目がわかるように`<tbody>`間に余白を設けたいのですが、`margin`を指定しても効きません。

```vuejs
<script setup>
import { ref } from 'vue'

const userList = ref([
  { name: "John", age: 12 },
  { name: "Mike", age: 54 },
  { name: "Hanako", age: 23 }
])
</script>

<template>
  <table>
    <tbody v-for="(user, index) in userList" :key="index">
      <tr>
        <th>名前</th>
        <td>{{ user.name }}</td>
      </tr>
      <tr>
        <th>年齢</th>
        <td>{{ user.age }}</td>
      </tr>
    </tbody>
  </table>
</template>

<style scoped>

/* tbodyにmarginを指定しても効かない */
tbody {
  margin-bottom: 1rem;
}


/* 後略 */
</style>
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911578/32d6f839-0f1d-467c-a0be-821dca0700c3.png)

**二番目以降の`<tbody>`に疑似要素を指定することで上手くいった**ので、紹介します。

:::note
サンプルコードはVue.jsを用いていますが、本題では特にVue.jsに限定した機能は用いていません
:::

## 疑似要素を用いた解決策

結論から示すと、以下で上手くいきます。

```css
tbody:not(:first-of-type)::before {
  content: "";
  display: block;
  height: 1rem;
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/911578/265f0037-71e6-439f-ba89-7ea98c40aa1f.png)

### 解説

- `:first-of-type`
    - 兄弟要素の中の最初の1個
    - 今回は最初の1個**以外**の上に余白が欲しいので、`:not`を指定
- `::before`
    - 実際のDOMにはないが、指定した要素の前に疑似的に要素を作り出す
- `content: "";`
    - 疑似要素を表示させるために必要。空で良い
- `display:block;`
    - `::befoe`は`inline`なので高さを指定できるように`block`に変更
- `height: 1rem;`
    - これで疑似的な余白を作成（remでなくてもOKです）

## サンプルコード全体

- [Vue Playgroundでの動作確認](https://play.vuejs.org/#__PROD__eNp9VEuLE0EQ/ivFiOyDTJJ9XcbsgsqCLr5Qb7aHzkxP0puZ7qG7k00IOWRRxMeCeBE8uuhBQS8eXET/zGzE/RdW9+QlhjCHqa766uuvqrur713NsnKnzbzAq+lQ8cyAZqad7RHB00wqA31QLIYBxEqmsILQFSKICKXQBtqaqVscjV0LWn1EBCBe0JQFQLwD2RTEKwFt4HJjEwalf+O3eYtN4zvb/8VvUEFbcorY3IIBEY/XiKhVCqkoEheGpVlCDcMVQM3QelKYdlGXUQ86fizVLvFWrdwScBGx7hr+pvKJB0GL9RDiYsQb51sGNbXtqrk3en0yen5Sq6A5H4j2+n3HV7bqYTBARDSjqczxLOA8+3bx8/0STqx/CSWats6i/sqkAWjNGoNLbXoJAx3KjFkWIlxSIKRZDWKutPFl7JtextaCoM6wYwz6lhFP2jBh7IEQ74r1RFwjby+AeiLDlnM1GW80EbOhWIoOPCciKuvgtsiHn1OqGlzkx29+v3o2+vIuH77Nhx/z4+PRi7N8+DIffsqHT2C9YnPGuoq9izy/Lo2R6Ty7xbodzn98OP+ODF//nD69OP3lSFwHCoIjHpkmJlarl53OlHb9sW+7Ws26Y6fdJYAq0LaRzhVj0X5MU55gmZoK7eMx8NjF6jRsNZRsi8gPZSJVAJfiyH5FVKqIKRtJaKbx2k6sSVtMswQmKuRlNIq4aODW5Z2iNoAOU4aHNPFpwhuoyshsjhhrybqgZcIjuBSG4Yy1YFwkrmo/xzEufct1o8gbK1mUF9t6HQofnL097tp4Jc/gLRIxb5QPtRQ4ORwD8UKZZjxh6m5mOI4H4gUFt43RJJFHB85nVJu5l+5ymixsLfAfanyG9s7dUwxb37GDYhIzeF4MH60N7z+4w7poT4OpjNoJopcE7zPsX9tqLGDXsGqUPYdzam+6+YfH81Dvd/EJ6ElRVqhFFvOKeDgTry8pfSZ3q7zt8rCr3uAv4IffKQ==)

<details><summary>サンプルコード全文</summary>

```vuejs
<script setup>
import { ref } from 'vue'

const userList = ref([
  { name: "John", age: 12 },
  { name: "Mike", age: 54 },
  { name: "Hanako", age: 23 }
])
</script>

<template>
  <table>
    <tbody v-for="(user, index) in userList" :key="index">
      <tr>
        <th>名前</th>
        <td>{{ user.name }}</td>
      </tr>
      <tr>
        <th>年齢</th>
        <td>{{ user.age }}</td>
      </tr>
    </tbody>
  </table>
</template>

<style scoped>

tbody:not(:first-of-type)::before {
  content: "";
  display: block;
  height: 1rem;
}

/* tbodyにmarginを指定しても効かない */
/*
tbody {
  margin-bottom: 1rem;
}
*/

/* 以下は装飾 */
table {
  width: 100%;
  max-width: 400px;
  margin: 0 auto;
  font-family: sans-serif;
  background-color: #fdfdfd;
  border-collapse: collapse;
}

th, td {
  padding: 0.5rem;
  vertical-align: top;
  border: 1px solid #ccc;
}

th {
  background-color: #f0f0f0;
  width: 30%;
}

td {
  background-color: #fff;
}

</style>
```

</details>

## おわりに

`<tbody>`の下に`<span>`を作ったり色々試したのですが上手くいかず。なんとか解決して良かったです。

ちなみに、今回の本筋とは関係ないのですが、`<tbody>`は複数作成しても良いらしいです。

- [MDN `<tbody>`: テーブル本体要素](https://developer.mozilla.org/ja/docs/Web/HTML/Reference/Elements/tbody)

> 連続していれば、1 つの表の中に複数の `<tbody>`を使用することができます。これにより必要に応じて、巨大な表の行を複数のセクションに分割し、個別に整形することができます。

## 参考

- [MDN ::before (:before)](https://developer.mozilla.org/ja/docs/Web/CSS/::before)
- [MDN :first-of-type](https://developer.mozilla.org/ja/docs/Web/CSS/:first-of-type)
- [【CSS】疑似要素でtheadとtbodyの間に隙間を空ける](https://www.ofuta.me/css/182)