概要
Tailwind CSS の ~
一般兄弟結合子(チルダ)を使う方法が見当たらなかったので、試してみます。
今回は Tailwind CSS で一般兄弟結合子を使って React 内でタブレイアウトを再現してみます。
注意: 例となるタブレイアウトは、アクセシビリティーを考慮していません
着地点
以下をパッと見で法則がわかった方は、
コピペしてご自身の組みたいレイアウトに当てはめてみてください。
// 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 ~ &')
})
]
}
ハマりポイント
まず optional
が何かわからないと、用途がわからないですね。
ここの optional
は CSS の擬似クラス です。
:optional は CSS の擬似クラスで、 required 属性が設定されていない
<input>
,<select>
,<textarea>
要素を表します。
https://developer.mozilla.org/ja/docs/Web/CSS/:optional
使用例は以下です。
input 要素は初期値に required
がないので、peer
class を持っていれば <div />
の内容が表示されます。
<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 が効いています。
つまり、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
ある場合は
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;
}
data-hoge の有無で要素の表示 / 非表示が可能になりました
直後である必要はない パターン(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 同様に多用は無用ですが、要件に合わせて利用していきたいですね。