POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook
Liam Johnston

本記事は、原著者の許諾のもとに翻訳・掲載しております。

:has()疑似クラスは筆者が断トツで一番気に入っているCSSの新機能です。筆者と同じ意見の読者も多いでしょう。少なくとも、State of CSSのアンケートに回答した方の中には多くいるはずです。セレクタを逆向きに指定できることで、これまでできると思いもしなかったようなすごいことがもっと可能になります。

「もっと」と言うのは、すでに多くの人が極めてスマートなアイデアを色々と発表しているからです。以下に一部紹介します。

この記事は、:has()の使い方に関する包括的なガイドではありません。すでに誰かが語ったことを繰り返しているわけでもありません。ちょっとだけ流行に乗って(hi👋)筆者が考えている:has()の使い方を紹介できればと思い、この記事を書いています。ただし、実際に使うのはブラウザ側の対応がもう少し進んでからですが。(Firefoxだけが未対応ですが、もうすぐ対応すると思います。)(※訳註 2024年8月現在は対応済)

その日が来れば、間違いなく:has()を使いまくるでしょう。筆者が最近ビルドした実例から、:has()の恩恵を特に受けそうなものをいくつか紹介します。

JavaScriptコンポーネントの外部要素と連携しなくて済む

ページの他の部分のスタイルを変える必要のあるインタラクティブコンポーネントを作成したことはありますか? 次の例をご覧ください。ここで、<nav>メガメニューを表し、開くとその上の<header>コンテンツの色が変わります。

筆者の仕事ではこうした処理がしょっちゅう必要になります。

これは、筆者があるサイトで使用するために作ったReactコンポーネントです。この例では、document.querySelector(...)を使用してページのReact以外の部分と連携し、<body><header>または別のコンポーネントのクラスを切り替える必要がありました。この世の終わりというほどのことではありませんが、面倒であることに違いありません。全部Reactで記述したサイト(Next.jsサイトなど)であっても、menuIsOpenのstateをコンポーネントツリーのずっと上の方で管理するか、あまりReact的ではありませんが、先程挙げた例と同じように、DOM要素の取得とクラスの切り替えを行う必要があります。

この問題は、:has()を使うことで解決します。

header:has(.megamenu--open) {
  /* style the header differently if it contains 
    an element with the class ".megamenu--open"
  */
}

JavaScriptコンポーネントの他のDOM要素をいじる必要はもうありません!

ストライプテーブル作成UXの向上

1行おきに背景色を追加した「ストライプテーブル」は、UXの向上に役立ちます。行を目で追いやすくなるため、表が見やすくなります。 しかし、筆者の経験では2、3行しかない表ではあまりうまく行きません。例えば、<tbody>が3行の表で「偶数」行に背景色を付けた場合、色付きの行は1行だけになります。そうするとあまり意味はなく、ハイライトされた1行に何か特別な意味があるのではないかと逆にユーザーを困惑させてしまうことになり兼ねません。

Bramus氏が:has()を使用して子の数に応じてスタイルを使い分ける方法を説明していますが、このテクニックを使用することで、4行以上ある表にストライプスタイルを適用することができます。

See the Pen Conditional table striping with :has() by Liam (@liamj) on CodePen.

もっと手の込んだことがしたければ、列の数が一定数以上の場合のみストライプスタイルを適用することもできます。

table:has(:is(td, th):nth-child(3)) {
  /* only do stuff if there are three or more columns */
}

テンプレートから条件ロジックを含むクラスを取り除く

ページの内容に応じてレイアウトを変えなくてはならないことがよくあります。以下のグリッドレイアウトをご覧ください。サイドバーの有無に応じて、メインコンテンツが配置されるグリッド領域が変わっています。

これは、CMSに兄弟ページが設定されているかどうかで変わる可能性があります。筆者は通常、両方のレイアウトに対応するため、テンプレートロジックを使用してレイアウトのラッパーにBEM修飾子クラスを条件付きで追加しています。その場合、CSSは次のようになります(簡略化のため、レスポンシブルールなどは省いています)。

/* m = main content */
/* s = sidebar */
.standard-page--with-sidebar {
  grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page--without-sidebar {
  grid-template-areas: '. m m m m m m m m m . .';
}

CSS的には、もちろんこれでも全く問題ありません。しかし、若干ごちゃごちゃしたテンプレートコードにはなってしまいます。テンプレートに使用する言語によっては、多くのクラスを条件付きで追加すると、かなり乱雑な見た目になってしまう場合があります。多くの子要素も使用しなければならない場合は特にそうです。

では、:has()を使ったやり方と比べてみてください。

/* m = main content */
/* s = sidebar */
.standard-page:has(.sidebar) {
  grid-template-areas: 's s s m m m m m m m m m';
}
.standard-page:not(:has(.sidebar)) {
  grid-template-areas: '. m m m m m m m m m . .';
}

正直なところ、CSS的にはそれほど大きな改善ではありません。しかし、HTMLテンプレートからモディファイアー部分のクラス名を省けるのはプラスだと思います。

:has()を使ったデザインの小技は簡単に思い浮かびます(例えば画像入りのカードなど)が、こうしたレイアウトの大きな変更にもかなり威力を発揮すると思います。

詳細度を管理しやすい

前回の記事を読んだ方であれば、筆者が詳細度にうるさいことはご存じかと思います。筆者のように、スタイル全体に:has()や:not()を追加することで詳細度が上がりすぎるのを避けたい場合は、:where()を使用するようにしましょう。

これは、:has()の詳細度は引数リストの中で最も詳細度の高い要素に基づくためです。したがって、IDのようなものが含まれていれば(なぜかは分かりませんが)、カスケード上でセレクタがオーバーライドされにくくなります。

一方、:where()の詳細度は常にゼロなので、詳細度が上がることはありません。

/* specificity score: 0,1,0.
  Same as a .standard-page--with-sidebar 
  modifier class
*/
.standard-page:where(:has(.sidebar)) {
  /* etc */
}

未来は明るい

これらはほんの一部ですが、本番環境で使えるようになるのが待ち遠しいです。(※訳註 2024年8月現在は対応済) CSS-Tricks Almanacでも多くの実例が紹介されています。皆さんは:has()を使ってどんなことがしたいですか? あなたが実際に直面したシチュエーションで、:has()が完璧な解決策になりそうなものはこれまでにありましたか?

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。