本連載について
はじめまして! サイボウズ フロントエンドエキスパートチームの麦島です。
本連載では、Webフロントエンドに関してもう一歩踏み込んだ知識について、サイボウズ フロントエンドエキスパートチームのメンバーによって不定期で解説記事を掲載していきます。モダンな仕様の紹介・
CSSの進化
本連載での最初のコンテンツは
CSSの表現力は年々新しい仕様の策定や実装とともに進化しています。従来であれば複雑なCSS定義が必要であったものが簡潔に表現できたり、JavaScriptを必要とした実装をCSSだけで表現できるケースも増えています。また、広く採用されてきたclass属性を用いた命名ルールやファイル分割といった設計手法を根本から覆すポテンシャルを持つ機能も利用可能となっています。
その中でも特にインパクトが大きい機能をいくつかピックアップして紹介します。2024年現在でモダンブラウザの最新版であれば利用可能な機能から、数年内に新たに使える見込みの高い機能まで幅広く取り上げていく予定です。
新しいCSS擬似クラス
第一回となる今回のテーマは:hover
や、フォーカス状態を表す:focus
、特定の条件を除外する:not()
などが挙げられます。
今回は、最近新しく利用可能となった擬似クラスの中でも特に押さえておくべきものを紹介します[1]。
:has()
擬似クラス/子孫要素に応じたスタイル適用
:has()
は、特定要素に対して指定した相対的な要素が存在するかを表現できる CSS 擬似クラスです。:has()
を用いることで、今までは難しかった
/*
img要素を子孫に持つ
a要素のカラーを変更
*/
a:has(img) {
color: red;
}
:has()
の利用例/セクション分割された入力フォームのスタイリング
:has()
について利用例を確認してみましょう。
この例では3つのチェックボックスが存在しており、それぞれがlabel
要素でラップされています。
<label> <input type="checkbox" /> HTML </label>
<label> <input type="checkbox" /> JavaScript </label>
<label> <input type="checkbox" /> CSS </label>
たとえばlabel
要素のスタイルを切り替えます。
:has()
を用いないJavaScriptによる実装
CSSではチェック状態は:checked
擬似クラスで判断できます。しかし:has()
を用いない場合、CSSのみで親に該当する要素に対してスタイルを適用する方法はありません。従来こういったケースでは、JavaScriptでチェックボックスの状態変化を監視し、CSSと組み合わせて対処されていました。
// チェック時にラベルのスタイルを変更するJavaScript
document.querySelectorAll("input").forEach((el) => {
el.addEventListener("change", () => {
// チェック状態に応じてラベルにスタイル用クラスを付与
el.checked
? el.closest("label").classList.add("checked")
: el.closest("label").classList.remove("checked");
});
});
/* 通常時のラベルのスタイル */
label {
background-color: #d3d3d3;
border-style: solid;
border-color: transparent;
}
/* チェック時のラベルのスタイル */
label.checked {
background-color: #ffffff;
border-color: #0000ff;
}
:has()
を用いたCSSのみでの実装
それでは、:has()
の例を見てみましょう。先ほどのケースで:has()
を用いると、CSSのみで期待するスタイルを実現できます。
/* 通常時のラベルのスタイル */
label {
background-color: #d3d3d3;
border-style: solid;
border-color: transparent;
}
/* 子孫要素がチェックされているときのラベルのスタイル */
label:has(:checked) {
background-color: #ffffff;
border-color: #0000ff;
}
これはlabel
要素の中でも:checked
擬似クラスを持つinput
要素を含む」
:has()
の利用例/レイアウトのスイッチ
もう一つ:has()
の利用例として、条件に応じたレイアウトのスタイル切り替えを考えてみましょう。
次のような条件での実装を想定します。
- ページにはサイドメニューが存在することがある
- サイドメニューが存在する場合、横並びの2カラムレイアウトとする
- サイドメニューが存在しない場合、1カラム表示で全体の
padding
を広げる - レイアウトには
grid
を利用する
サイドメニューが存在する場合のHTMLは次のような内容です。
<div class="layout">
<!-- サイドメニュー -->
<nav>
<h2>Navigation</h2>
<ul>
<li><a href="/html">HTML</a></li>
<li><a href="/js">JavaScript</a></li>
<li><a href="/css">CSS</a></li>
</ul>
</nav>
<!-- メインコンテンツ -->
<main>
<h1>Main Contents</h1>
<p>This is description</p>
</main>
</div>
このようなケースでも:has()
を用いることで、サイドメニューの有無に応じたスタイルの切り替えが容易に行えます。CSSは次のような内容です。
.layout {
display: grid;
grid-template-columns: 1fr;
padding: 8px;
}
/* サイドメニューが存在する場合 */
.layout:has(nav) {
grid-template-columns: 200px 1fr;
padding: 16px;
}
サイドメニューの有無に応じて新たにクラスを付与することなく、:has()
を用いてCSSのみでレイアウトの切り替えが実現できました。
:is()
擬似クラス/複数セレクタの一括指定
:is()
は、複数のセレクタのうちいずれかがマッチする要素を選択できる擬似クラスです。これにより、従来よりも簡潔かつ重複の少ないCSSを記述できます。
/*
foo、bar、bazのいずれかのクラスを持つ
要素の子孫のa要素のカラーを変更
*/
:is(.foo, .bar, .baz) a {
color: red;
}
:is()
の利用例/ネストした複数条件の要素へのスタイリング
次のような仕様を満たすCSSを例に考えます。
- ヘッダとフッタの直下に存在するリンクとテキストは、文字サイズを小さくする
- 特定のページにのみ適用し、該当ページの
body
要素には.page-a
または.page-b
クラスが付与されている
HTMLは次のような内容です。
<body class="page-a">
<div class="wrapper">
<header>
<span>Header | </span>
<a href="#">Top</a>
<a href="#">Articles</a>
<a href="#">Search</a>
</header>
<main>
<span>Main | </span>
<a href="#">Article1</a>
<a href="#">Article2</a>
<a href="#">Article3</a>
</main>
<footer>
<span>Footer | </span>
<a href="#">Help</a>
<a href="#">Contact</a>
</footer>
</div>
</body>
期待する見た目は次のとおりです。
:is()
を用いない場合のCSS定義
:is()
を用いない場合、次のようなCSS定義が必要です。
/* ヘッダとフッタ配下に同じスタイルを適用するCSS */
.page-a header > a,
.page-a header > span,
.page-a footer > a,
.page-a footer > span,
.page-b header > a,
.page-b header > span,
.page-b footer > a,
.page-b footer > span {
font-size: 12px;
}
定義に重複が多いですね。実際のプロダクトコードでは、さらにネストが深くなることもありえます。また、重複が多いことに起因し、修正時のコストも大きくなりやすいです。たとえば、page-c
にも同じスタイルを適用したい」
:is()
を用いた場合のCSS定義
先程のケースにおいて:is()
を用いると、同等のCSSを次のように表現できます。
:is(.page-a, .page-b) :is(header, footer) > :is(a, span) {
font-size: 12px;
}
簡潔に表現できており、重複も少ないことがわかります。対応ページを増やしたくなっても、:is()
に指定するセレクタの書き換えだけで済みます。こういった課題に対して、従来ではPostCSSやSass:is()
を用いると、純粋なCSSのみで実装可能になります。
:is()
の利用例/複数の擬似クラスに対するスタイル適用
他にも、複数の擬似クラスに対してまとめてスタイルを適用する際にも:is()
が役立ちます。
たとえば、button
要素とa
要素に対してホバー時とフォーカス時に同じスタイルを適用したい」:is()
を用いると次のように表現できます。
:is(a, button) {
border-width: 1px;
border-style: solid;
border-color: gray;
}
:is(a, button):is(:hover, :focus) {
border-color: red;
}
「複数の状況で同じスタイルを適用したい」:is()
で効率よく記述できないか検討してみるとよいでしょう。
:where()
擬似クラス/詳細度を持たない一括指定
:is()
と似た擬似クラスとして、:where()
があります。:where()
は:is()
と基本的な部分は同じで、指定された複数セレクタにマッチする要素を選択できます。
/*
foo、bar、bazのいずれかのクラスを持つ
要素の子孫のa要素のカラーを変更
*/
:where(.foo, .bar, .baz) a {
color: red;
}
これだけだと:is()
と同じように見えます。しかし、唯一かつ大きな差異として、:where()
に記述したCSSは常に詳細度が0となるという特徴を持ちます。どういった際に恩恵を受けられるのか、例を見てみましょう。
:where()
の利用例/共通CSSの定義と上書き
CSS実装において、広範囲で共通となる定義を用意し、全ページで読み込むケースはよく見られます。先述の:is()
の例と同様のHTMLに対して、次のようにヘッダとフッタ配下のリンクのフォントサイズを指定する共通CSSが定義されているとします。
/* 共通CSS */
header a,
footer a {
font-size: 12px;
}
問題点:詳細度で上回れないと上書きできない
この共通CSSをロードした状態で、
/* ページ固有のCSS */
a {
font-size: 20px;
}
しかしこれはうまくいきません。次の図のように、ヘッダとフッタ配下のリンクについては文字サイズが小さいまま表示されてしまいます。
原因を理解するために、CSS詳細度の基本を一度おさらいしておきましょう。
CSS詳細度
1つの要素に対してCSSはいくつも定義できますが、カスケードと呼ばれるルールに従い、セレクタや定義順などさまざまな条件によってスタイルが適用される優先順位は変化します。その優先順位を判断するためにブラウザで用いられるアルゴリズムのひとつがCSS詳細度です。
CSS詳細度では、セレクタを0-1-2
や0,1,2
といった形で記述されることがあります。
仮に#app .main.
といったセレクタの場合、詳細度は1-2-1
となります
例に挙げた共通CSSでのヘッダ内リンクへの定義では、セレクタはheader a
です。IDやクラスの指定はなく要素を2つ指定しており、これは詳細度で表すと0-0-2
です。一方、ページ固有の上書き用CSSのセレクタはa
です。要素1つの指定で、これは詳細度で表すと0-0-1
です。重みは左から順に比較され、一番右側の2
と1
の差により、ページ固有CSSより共通CSSの重みのほうが大きいと判断されます。よって、a
のみのセレクタでは共通CSSの定義を詳細度で上回れず、文字サイズは小さいままの表示となってしまいます。
/* 共通CSS */
/* 詳細度: 0-0-2 */
header a,
footer a {
font-size: 12px;
}
/* ページ固有のCSS */
/* 詳細度: 0-0-1 */
a {
font-size: 20px;
}
:where()
を利用した場合
では、共通CSSを:where()
を用いる形に変更してみます。
/* :where()を用いた共通CSS */
:where(header, footer) a {
font-size: 12px;
}
実際の見た目は次の通りです。
期待通りページ固有のCSSが適用され、文字サイズが大きくなりました。
:where(header, footer)
のCSS詳細度は0として扱われます。:where()
を利用しない場合の詳細度は0-0-2
でしたが、:where()
を用いるとa
のみの重みで計算されるため0-0-1
となります。ページ固有CSSと共通CSSで詳細度が同じとなりますが、詳細度が同じ場合、よりあとで定義しているものが適用されます。ページ固有CSSを共通CSSよりあとに定義するか、ファイルが分割されている場合にはあとで読み込むことで、期待どおり上書きできます。
/* :where()を用いた共通CSS */
/* 詳細度: 0-0-1 */
:where(header, footer) a {
font-size: 12px;
}
/* ページ固有のCSS */
/* 詳細度: 0-0-1 */
a {
/*
* 詳細度が同じため、
* より後で定義されているこちらが適用される
*/
font-size: 20px;
}
CSS実装時、詳細度に起因する問題と遭遇することは珍しくありません。詳細度で上回るためだけにclass属性を付与したり、最悪!important
で強制的に適用するケースもありえます。:where()
をうまく活用すると、上書きする側では従来より詳細度を意識する必要が減り、より保守性の高い形でCSSを定義できます。
昨今ではライブラリやフレームワークが内部で:where()
を活用しているケースも珍しくありません。たとえば、静的サイトジェネレータのAstroではコンポーネントのスタイルに:where()
を利用していたり、Panda CSS・:where()
の利用を確認できます。既存コードの理解を深めるうえでも、:where()
の挙動を把握しておくことは大事だと言えるでしょう。
ブラウザでのサポート状況
今回紹介した擬似クラスについて、本記事掲載時点でのメジャーブラウザにおける利用可能バージョンは次の通りです。これ以降のバージョンであれば利用可能です。
機能 | Chrome | Edge | Safari | Firefox |
---|---|---|---|---|
:has() |
105 | 105 | 15. |
121 |
:is() |
88 | 88 | 14 | 78 |
:where() |
88 | 88 | 14 | 78 |
まとめ
今回は:has()
・:is()
・:where()
の3つの擬似要素を紹介しました。
JavaScriptを用いずにCSSのみでスタイリング可能な幅が増えることで、ファイルサイズの軽減や実行速度の改善といったユーザー体験にもつながります。また、SSR