Tailwind CSS addVariant の ~ 一般兄弟結合子でCSSのみのタブを作る

# 概要

Tailwind CSS の `~` [一般兄弟結合子（チルダ）](https://developer.mozilla.org/ja/docs/Web/CSS/General_sibling_combinator)を使う方法が見当たらなかったので、試してみます。


今回は Tailwind CSS で一般兄弟結合子を使って React 内でタブレイアウトを再現してみます。
:exclamation:注意: 例となるタブレイアウトは、アクセシビリティーを考慮していません


# 着地点


以下をパッと見で法則がわかった方は、
コピペしてご自身の組みたいレイアウトに当てはめてみてください。

![スクリーンショット 2022-09-02 7.19.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90188/e6af3f60-72ed-2b06-7e4f-818c83dcdbad.png)

```jsx
// components/Tab/Tab.tsx
export const Tab: React.FC = () => (
  <div>
    <label htmlFor="radio1">tab-1</label>
    <label htmlFor="radio2">tab-2</label>
    <label htmlFor="radio3">tab-3</label>
    <input type="radio" name="radio" id="radio1" className="peer peer-tab-1 hidden" defaultChecked />
    <input type="radio" name="radio" id="radio2" className="peer peer-tab-2 hidden" />
    <input type="radio" name="radio" id="radio3" className="peer peer-tab-3 hidden" />
    <div className="hidden peer-contents-1:block">contents1</div>
    <div className="hidden peer-contents-2:block">contents2</div>
    <div className="hidden peer-contents-3:block">contents3</div>
  </div>
)

```

```js
// tailwind.config.js
const plugin = require('tailwindcss/plugin')
module.exports = {
  ...
  plugins: [
    plugin(function ({ addVariant }) {
      addVariant('peer-contents-1', ':merge(.peer).peer-tab-1:checked ~ &')
      addVariant('peer-contents-2', ':merge(.peer).peer-tab-2:checked ~ &')
      addVariant('peer-contents-3', ':merge(.peer).peer-tab-3:checked ~ &')
    })
  ]
}
```
```css
/* 出力されるCSS */
...
.peer.peer-tab-1:checked ~ .peer-contents-1\:block {
  display: block;
}
.peer.peer-tab-2:checked ~ .peer-contents-2\:block {
  display: block;
}
.peer.peer-tab-3:checked ~ .peer-contents-3\:block {
  display: block;
}
```


# Docs のコード解説

続いて詳細に見てみます。以下の記述があります。


https://tailwindcss.com/docs/plugins#parent-and-sibling-states

> Your custom modifiers won’t automatically work with Tailwind’s parent and sibling state modifiers.

つまりCSSで書くような複雑な兄弟要素などは、自分で作る必要があります。（[group](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state) / [peer](https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-sibling-state) などの親 / 兄弟用クラスもあります）

> To support the group-* and peer-* versions of your own custom modifiers, register them as separate variants using the special :merge directive to ensure the .group and .peer classes only appear once in the final selector.

`:merge` を使って、`.peer` 等と紐づけると様々なセレクターを使用することが可能です。

## Docs `:Optional` の使い方　

サンプルコードを見てみます。

```js
// tailwind.config.js
const plugin = require('tailwindcss/plugin')
module.exports = {
  // ...
  plugins: [
    plugin(function({ addVariant }) {
      addVariant('optional', '&:optional')
      addVariant('group-optional', ':merge(.group):optional &')
      addVariant('peer-optional', ':merge(.peer):optional ~ &')
    })
  ]
}
```

:point_right: ハマりポイント
まず `optional` が何かわからないと、用途がわからないですね。
ここの `optional` は [CSS の擬似クラス](https://html.spec.whatwg.org/multipage/semantics-other.html#selector-optional) です。
> :optional は CSS の擬似クラスで、 required 属性が設定されていない `<input>`, `<select>`, `<textarea>` 要素を表します。
> https://developer.mozilla.org/ja/docs/Web/CSS/:optional


使用例は以下です。
input 要素は初期値に `required` がないので、`peer` class を持っていれば `<div />` の内容が表示されます。

![スクリーンショット 2022-09-02 9.17.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90188/4eef9f60-c887-7c51-3b88-068eb98bde9c.png)


```jsx
<input
  type="text"
  className="peer optional:border optional:border-solid"
/>
<div className="hidden peer-optional:block">
  optional!!
</div>
```

```css
/* 出力されるCSS */
...
.optional\:block:optional {
  display: block;
}
.optional\:border:optional {
  border-width: 1px;
}
.optional\:border-solid:optional {
  border-style: solid;
}
.peer:optional ~ .peer-optional\:block {
  display: block;
}

```
`<input />` 自身には border が効き、後方にある兄弟要素`<div />`の `peer-optional` の block が効いています。

:point_right: つまり、Docs のコードは、


- `addVariant('optional', '&:optional')`
    optional属性があった場合、optional属性にスタイル（optional:border optional:border-solid）を適用する
- `addVariant('peer-optional', ':merge(.peer):optional ~ &')`    
    `.peer:optional`後方にある兄弟要素に peer-optional にスタイル（peer-optional:block）を適用する ( & は peer-optional　自身） 

と読めます。

気になる方は以下を追ってみてください。


https://github.com/tailwindlabs/tailwindcss/blob/58cc7ed3e8ba47fbc5db4387b39685dea37b4a16/src/lib/setupContextUtils.js#L450

## :required があった場合


では、`required`ある場合は

![スクリーンショット 2022-09-02 9.21.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90188/a0a42a3a-55d1-89f2-1251-08de9b12bec5.png)

> required 属性が設定されていない `<input>`, `<select>`, `<textarea>` 要素を表します。

なので `required` がある場合は、
当然何も表示されなくなります。


```jsx
<input
  type="text"
  className="peer optional:border optional:border-solid"
/>
<div className="hidden peer-optional:block">
  optional!!
</div>
```


## Click イベントと合わせた応用

もっと具体的な条件があった場合、 [data 属性](https://developer.mozilla.org/ja/docs/Learn/HTML/Howto/Use_data_attributes) の data-hoge のような形でも利用できます。
ボタンを押したら、要素を表示する方法を絡めてみます。



```js
const plugin = require('tailwindcss/plugin')
module.exports = {
  // ...
  plugins: [
    plugin(function({ addVariant }) {
      addVariant('data-hoge', '&[data-hoge="true"]')
      addVariant('group-data-hoge', ':merge(.group)[data-hoge="true"] &')
      addVariant('peer-data-hoge', ':merge(.peer)[data-hoge="true"] ~ &')
    })
  ]
}
```
```jsx
// components/click.tsx
const ToggleClick: React.FC = () => {
  const [isClicked, setClick] = useState(false)
  const handleClick = () => {
    setClick(!isClicked)
  }

  return (
    <>
      <button
        type="button"
        className="peer"
        onClick={handleClick}
        data-hoge={isClicked}
      >
        Click me
      </button>
      <div className="hidden peer-data-hoge:block">isClicked!!</div>
    </>
  )
}
```

```css
/* 出力されるCSS */
...
.data-hoge\:border[data-hoge="true"] {
  border-width: 1px;
}
.data-hoge\:border-solid[data-hoge="true"] {
  border-style: solid;
}
.peer[data-hoge="true"] ~ .peer-data-hoge\:block {
  display: block;
}

```

![1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90188/e106fdc7-305a-7ec3-1cf4-a91b58936414.gif)

data-hoge の有無で要素の表示 / 非表示が可能になりました:grinning:

## 直後である必要はない パターン（selecter ~ selecter）

本題です。
今までは`<input />` の兄弟要素で記述していました。
続いて一般兄弟結合子例です。

```jsx
// components/Tab/Tab.tsx
export const Tab: React.VFC = () => (
  <div>
    <div data-layout="label-wrapper">
      <label htmlFor="radio1">tab-1</label>
      <label htmlFor="radio2">tab-2</label>
      <label htmlFor="radio3">tab-3</label>
    </div>
    <input type="radio" name="radio" id="radio1" className="peer peer-tab-1 hidden" defaultChecked />
    <input type="radio" name="radio" id="radio2" className="peer peer-tab-2 hidden" />
    <input type="radio" name="radio" id="radio3" className="peer peer-tab-3 hidden" />
    <div data-layout="contents-wrapper">
      <div className="hidden peer-contents-1:block">contents1</div>
      <div className="hidden peer-contents-2:block">contents2</div>
      <div className="hidden peer-contents-3:block">contents3</div>
    </div>
  </div>
)
```

```js
// tailwind.config.js
const plugin = require('tailwindcss/plugin')
module.exports = {
  ...
  plugins: [
    plugin(function ({ addVariant }) {
      addVariant('peer-contents-1', ':merge(.peer).peer-tab-1:checked ~ [data-layout="contents-wrapper"] &')
      addVariant('peer-contents-2', ':merge(.peer).peer-tab-2:checked ~ [data-layout="contents-wrapper"] &')
      addVariant('peer-contents-3', ':merge(.peer).peer-tab-3:checked ~ [data-layout="contents-wrapper"] &')
    })
  ]
}
```
```css
/* 出力されるCSS */
...
.peer.peer-tab-1:checked ~ [data-layout="contents-wrapper"] .peer-contents-1\:block {
  display: block;
}
.peer.peer-tab-2:checked ~ [data-layout="contents-wrapper"] .peer-contents-2\:block {
  display: block;
}
.peer.peer-tab-3:checked ~ [data-layout="contents-wrapper"] .peer-contents-3\:block {
  display: block;
}
```

Tailwind CSS というより CSS の知識ですね。
コンテンツとなる部分を `<div />` で括ってもどうってことはなく、
`tailwind.config.js` 側も入れ子にしただけです。

---

利用してみるとあっさり理解できました。
Attribute Value 同様に多用は無用ですが、要件に合わせて利用していきたいですね。