2024年10月19日
ReactコードをCSSの:hasセレクタで置き換える
本記事は、原著者の許諾のもとに翻訳・掲載しております。
CSSの新しい:hasセレクタと、これを使用したReactコードの改善方法について説明します。実用的で美しい例とともに。
大昔、とは言ってもCSSが出てきた当初の話ですが、CSSはカスケードする仕組みになっていると教えられていました。それは、Cascading Style Sheetsという名前からも分かります。CSSでは、入れ子のように要素の中の要素を指定し、さらにその中に含まれる要素を指定していくことができます。しかし、その逆はできません。したがって、子要素が親要素にスタイルを適用するには、JavaScriptを使うしかありませんでした。
今までは。
すべての主要ブラウザがCSSの:hasセレクタに対応したことで、親要素を指定できるようになりました。それだけではありません。これは世界が一変したと言えるほどの出来事です。筆者のように、要素の角を丸くするために透過GIFを使用していた時代からウェブ開発を行っている読者の方は、これによって広がる可能性に圧倒されるでしょう。
これを使ってぜひ色々と遊んでみていただきたいと思いますが、Reactの世界では実際にどのような実用的用途があるのでしょうか。ここでは特に注目すべき用途を3つ紹介したいと思います。
:hasセレクタとは?
従来のCSSでは、以下のようなことができます。
.content .card {
background: #f0f0f0;
}
.content img {
margin: 1rem 0;
}
これにより、.content
要素に含まれる.card
要素の背景が薄いグレーに変わり、中の画像に余白が追加されます。そうすることで、画像とテキストが視覚的に区別されます。
また、隣の兄弟を+
または~
結合子で指定することができます。例えば、上記の画像が.card
要素のすぐ後にくる場合、より見やすくするために余白をさらに追加するとよいでしょう。
// images that immediately follow a card element
// will have bigger margins than other images
.content .card + img {
margin: 2rem 0;
}
しかし、最近まで「逆」方向に要素を指定することができませんでした。例えば、すぐ後に画像が続く.card
要素の背景を変えたい場合、JavaScriptを使うしか方法がありませんでした。同様に、中に画像が含まれる場合に.card
要素のスタイルを変えることもできませんでした。
新しいCSSセレクタ:has
は、この問題を解決します。
例えば、中に画像が含まれる.card
要素にはピンク色の枠を使用し、それ以外の.card
要素にはグレーの枠を使用するといったことも簡単にできます。
// all the cards will have grey top borders
.card {
border-top: 10px solid #f6f7f6;
}
// cards with images inside will have pink borders
.card:has(img) {
border-top: 10px solid #fee6ec;
}
:has
セレクタを他のセレクタと組み合わせて使用することもできます。すぐ後に画像が続くカードには青い枠を付けてみましょう。その場合は+
結合子で条件を追加します。
// if a card has an image as a next element - give it a blue border
.card:has(+ img) {
border-top: 10px solid #c4f4ff;
}
もっと複雑なコードを書くこともできます。h3タグを含まず、img
タグを含み、別の.card
要素がすぐ後に続き、それより後にimg
タグがある.card
要素の背景を、複数の画像を含むカードが後に続く場合に限り緑色にしてみましょう。
// have fun reading that ;)
.card:not(:has(h3)):has(img):has(+ .card):has(~ img):has(~ .card):has(> img:nth-child(1)) {
background-color: #c3dcd0;
}
以下が実装例になります。
しかし、Reactアプリでこのようなことがしたいでしょうか。こうしたスタイルは、子セレクタと同様に要素の境界が曖昧になりがちです。ここ10年くらい、そうならないようにするための方法を色々と工夫して編み出してきたのではなかったでしょうか。BEM、SASS、CSS-in-JS、CSSモジュールなどなど。スタイルの適用範囲を意図した要素にのみ限定するために、私たちはあらゆる手段を講じます。
では、突如その流れに逆行し、あらゆるベストプラクティスと逆のことをする理由はどこにあるのでしょうか。もちろん、Reactでは数年置きにそうした傾向が見られるのはありますが😅
答えは、複雑なReactコードを不要にするためです。最高のReactコードは、Reactコードを一切使わないことである場合もあるのです。先ほどは極めて複雑なセレクタの例をお見せしましたが(まね 真似することはお勧めしません)、ReactをCSSに置き換えることでロジックを単純化することができ、若干パフォーマンスを改善できる場合もあります。
ではいくつか実例を見てみましょう。
:hasセレクタと要素のフォーカス状態
タスクボードを実装したいとします。ボード上には多数のカードがあり、各カードにはそれぞれ「開く」と「削除」の2つのボタンがあります。「開く」ボタンをクリックすると、カードの内容がすべてモーダル上に開きます。「削除」をクリックするとカードが削除されます。
このカードのコードは非常にシンプルです。
<div className="card">
Some text here
<div className="buttons">
<button>
<Open />
</button>
<button>
<Delete />
</button>
</div>
</div>
Tabキーを使ってボタンに移動するなど、キーボードで操作可能にしたいと思います。さらに、キーボードユーザーの利便性を改善するため、どのボタンが選択されているかが分かるようにしたいと思います。ユーザーがTabキーを使っていずれかのボタンに移動したら、カードが少し「飛び出す」ようにし、同じ列の他のカードはグレーアウトさせます。さらに、アクティブ状態のカードの枠の色を変え、現在アクティブな操作が分かるようにしたいと思います。「削除」ボタンには赤、「開く」ボタンには緑を使用します。
Reactでこの機能を実装するには、focusイベントリスナーを追加し、現在アクティブなボタンを検知し、カード自体のclassNamesを変更できるようstateを維持し、他のカードも変更できるようそのstateをどうにかして親と共有する必要があります。そのためには、Contextなどの状態管理ソリューションを導入する必要があるでしょう。気が付けば、すべてのタブのすべてのカードを再レンダリングする本格的なフォーカスマネージャーを実装しなくてはならなくなっているでしょう。実際にこのような凝ったインタラクションを見かけないのも無理はありません。せいぜいボタンの輪郭線に一貫性をもたせることぐらいしか望めないでしょう。
しかし、:has
セレクタを使えば今述べたようなことも比較的簡単に実装できます。
ステップ1。className
に頼らなくても選択できるよう、data-
属性をボタンに割り当てます。
// add data-action attributes to the buttons
<button data-action="open">
<Open />
</button>
<button data-action="delete">
<Delete />
</button>
ステップ2。フォーカスされた「削除」ボタンを含むカードを見つけ、CSSを変えます。
// make the card "pop" and change its border colors
// if the "delete" button inside the card is focused
.card:has([data-action='delete']:focus-visible) {
border-top: 10px solid #f7bccb;
box-shadow: 0 0 0 2px #f7bccb;
transform: scale(1.02);
}
ステップ3。フォーカスされた「開く」ボタンを含むカードを見つけ、CSSを変えます。
// make the card "pop" and change its border colors
// if the "open" button inside the card is focused
.card:has([data-action='open']:focus-visible) {
transform: scale(1.02);
border-top: 10px solid #c3dccf;
box-shadow: 0 0 0 2px #c3dccf;
}
ステップ4。これが一番難しい処理になります。フォーカスされた「開く」または「削除」ボタンを含むカードの「前後」のカードをすべて見つけ、グレーアウトする必要があります。魔法の種明かしはこちらです。
// all cards after the card with focused "open" button
.card:has([data-action='open']:focus-visible) ~ .card,
// all cards after the card with focused "delete" button
.card:has([data-action='delete']:focus-visible) ~ .card,
// all cards before the card with focused "open" button
.card:has(~ .card [data-action='open']:focus-visible),
// all cards before the card with focused "delete" button
.card:has(~ .card [data-action='delete']:focus-visible) {
filter: greyscale();
background-color: #f6f7f6;
}
JavaScriptもReactの再レンダリングも一切必要としない、どのボードよりも美しいキーボード操作を実現できました。以下の実例を触ってみてください。
in all the boards ever with zero JavaScript and zero React re-renders! A live example is below to play around with.
:hasセレクタとカテゴリ分け
もう一つ、:has
セレクタのユースケースとして筆者がシンプルで秀逸だと思うのが、データに基づいて要素を色分けできる点です。
例えば、ショップで販売している商品を記載した表を実装してみましょう。これらの商品は、特定のカテゴリに分類されます。オンラインショップで事務用品、衣類、馬を販売しているとしましょう。表にはいくつかの列があり、最もシンプルな形にまとめたものが以下になります。
これをコードにすると、以下のようになります。
...
<tr>
<td>Socks</td>
<td>Created by...</td>
<td>Inventory full</td>
<td>
<span className="category">
clothes
</span>
</td>
</tr>
...
では、表の左側境界線にカテゴリを示す色を付けることで、各行のカテゴリを色分けしたいと思います。また、在庫切れの商品については、行の背景を赤く色付けすることで目立たせたいと思います。これを実装したものが以下になります。
Reactでは、propsを使用し、カテゴリや在庫に関する情報を少なくともrow
タグ、あるいは最初のセルに渡す必要があります。さらに、バリエーションごとにクラス名や、場合によっては内部コンポーネントを作成する必要があります。この例では、こうした複雑な処理は一切不要です。
では、一連の流れを見てみましょう。
ステップ1。情報を格納したdata-
属性を、すでにその情報を持っているセルに追加します。
<tr>
<td>Socks</td>
<td>Created by...</td>
<!-- add data-inventory attribute here -->
<td data-inventory="full">Inventory full</td>
<td>
<!-- add data-category attribute here -->
<span className="category" data-category="clothes">
clothes
</span>
</td>
</tr>
ステップ2。:has
を使用し、必要なカテゴリを色分けします。
data-category
を持つ要素を含む行の最初のセルに、カテゴリによって異なる色の境界線を追加します。
.table tr:has([data-category='clothes']) td:first-child {
border-left: 6px solid #f7bccb;
}
.table tr:has([data-category='office']) td:first-child {
border-left: 6px solid #f4d592;
}
.table tr:has([data-category='animals']) td:first-child {
border-left: 6px solid #c4f4ff;
}
data-inventory
の値がempty
の要素を含む行には、赤色の背景を追加します。
.table tr:has([data-inventory='empty']) {
background: #f6d0ce;
}
美しく色分けされた表の出来上がりです。この表で特に素晴らしいのが、動的な状態にあり頻繁に更新される属性であっても、行全体を再レンダリングして色を更新する必要がない点です。再レンダリングされるのはdata-
属性を含むセルだけです。よりシンプルで洗練されたコードに加え、この点も若干パフォーマンスの向上に寄与する可能性があります。
以下のインタラクティブな例をご覧ください。
:hasセレクタとフォーム
次が今回紹介する:has
セレクタの最後のユースケースです。フォーム要素の状態に応じて要素のスタイリングを行うというものですが、これが大変効果的で筆者も非常に気に入っています。
例えば、入力を無効にできるフォームにおいて、入力欄のラベル表示や説明も視覚的に「無効」にすることができます。
このフォームを記述したコードが以下になります。
<form className="form">
<fieldset>
<label htmlFor="form-name">Name</label>
<input type="text" name="name" id="form-name" disabled value="Nadia" />
<div className="description">Just your first name is fine</div>
</fieldset>
<fieldset>
<label htmlFor="form-email">Email Address</label>
<input type="email" name="email" id="form-email" required />
<div className="description">We don't accept gmail domains!</div>
</fieldset>
</form>
CSSでは、stateが:disabled
の入力を含むfieldset
を指定し、label
要素と.description
要素のスタイリングを行います。
fieldset:has(input:disabled) label,
fieldset:has(input:disabled) .description {
color: #d6d6d6;
}
もちろん、フォーカスも使用できます。入力がフォーカスされると左側に線が表示されるようにしたい場合、
次のようにするだけです。
fieldset:has(input:focus-visible) {
border-left: 10px solid #c4f4ff;
}
もしくは、チェックボックス付きのリストを実装する場合、チェックされた行を強調表示するには通常ならチェックボックスのstateを格納し、.active
クラスを作成しますが、こうした処理を行うことなく簡単に強調表示することが可能です。
必要なのはCSSセレクタだけです。
.list-with-checkboxes li:has(input:checked) { background: rgba(196, 244, 255, 0.3); }
こちらのライブプレビューをご覧ください。
素晴らしいと思いませんか?お気に入りの:has
の使い方がある方は、ぜひコメントで共有してください!
もちろん、セレクタを使用してReactコードを単純化する方法は他にもたくさんあります。ここに挙げたのは、筆者が特に気に入っているごく一部の例にすぎません。:has
セレクタについてもっと学び、色んな実例を試してみたい方は、例が豊富で内容的にも良い記事を以下に挙げていますので、ぜひ参照してみてください。
- CSS :has Parent Selector
- Meet :has, A Native CSS Parent Selector (And More) — Smashing Magazine
- Level Up Your CSS Skills With The :has() Selector — Smashing Magazine
- Using :has() as a CSS Parent Selector and much more
- Style a parent element based on its number of children using CSS :has()
ここ数年のCSSの進歩を考えると、5年後くらいにはReactが必要なくなっているかもしれません。🤯もしそうなれば面白いですね!
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa