1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-03

概要

Tailwind CSS の ~ 一般兄弟結合子(チルダ)を使う方法が見当たらなかったので、試してみます。

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

着地点

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

スクリーンショット 2022-09-02 7.19.39.png

// 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>
)

// 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 */
...
.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 のコード解説

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

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

つまりCSSで書くような複雑な兄弟要素などは、自分で作る必要があります。(group / peer などの親 / 兄弟用クラスもあります)

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 の使い方 

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

// 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 が何かわからないと、用途がわからないですね。
ここの optionalCSS の擬似クラス です。

: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

<input
  type="text"
  className="peer optional:border optional:border-solid"
/>
<div className="hidden peer-optional:block">
  optional!!
</div>
/* 出力される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 自身)

と読めます。

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

:required があった場合

では、requiredある場合は

スクリーンショット 2022-09-02 9.21.22.png

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

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

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

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

もっと具体的な条件があった場合、 data 属性 の data-hoge のような形でも利用できます。
ボタンを押したら、要素を表示する方法を絡めてみます。

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"] ~ &')
    })
  ]
}
// 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 */
...
.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

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

直後である必要はない パターン(selecter ~ selecter)

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

// 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>
)
// 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 */
...
.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 同様に多用は無用ですが、要件に合わせて利用していきたいですね。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?