🙌

:is(), :where() を活用してCSSを「後置修飾」で書く

2023/08/06に公開

はじめに結論から

:is()または:where()を使うと、 これまで「前置修飾」で書かなきゃいけなかったセレクタが 「後置修飾」で書けて嬉しい!

/* 通常のスタイル */
.button {}

/* 特定の要素と隣接するときのスタイル */
/* こう書いてたものが */
.link + .button {}
/* こう書ける */
.button:is(.link + *) {}

/* 特定の要素に内包されているときのスタイル */
/* こう書いてたものが */
header .button {}
/* こう書ける */
.button:is(header *) {}

/* 特に、「特定のクラスの中の要素に対するスタイル」を条件分岐させるときに便利 */
.container p {}
.container p:is(h2 + *) {}
.container p:is(section *) {}

「前置修飾」「後置修飾」ってどういうこと?

すみません、自分で勝手に呼んでる言葉なので、説明します。

以下のセレクタは、どれも .button に対するスタイルを指定しています。
しかし、条件分岐(範囲の制限)のために、 .button の前に別のセレクタをつけています。
このパターンを 「前置修飾」 と呼ぶことにします。

.button {}
.link + .button {}
header .button {}

一方、次のように、擬似クラスや属性セレクタなど、 .button の後ろに別のセレクタをつけて、条件分岐をしている場合は、 「後置修飾」 と呼ぶことにします。

.button.button--primary {}
.button:first-child {}
.button:disabled {}
.button[aria-expanded="true"] {}

前置修飾の問題点

問題は「特定のクラスの中の要素に対するスタイル」を指定する場合に起きます。
後置修飾なら後ろにつなげればいいだけなのですが、
前置修飾は間に割り込む必要があります。

.container p という同じ要素のパターン違いなだけのはずなのに、全く違うセレクタになってしまいます。

.container p {}
/* 後置修飾なら後ろにつなげればいいだけ */
.container p:first-child {}
/* 前置修飾は間に割り込む必要がある */
.container h2 + p {}
.container section p {}

特に、Sass で記述する場合には、1つのブロックにまとめたいという要望が出てきます。

擬似クラス :first-child は後置修飾で書けるので ブロックの中でネストして & でつなげることができますが、
前置修飾の h2 + p& でつなげることができません。
これでは、p に対するスタイルの記述が 2 つのブロックに分かれてしまい、可読性が悪くなります。

.container {
  p {
    &:first-child {
    }
  }
  h2 + p {
  }
}

:is(), :where()で「後置修飾」

:is(), :where() はカッコ内に記述したセレクタに当てはまる要素を選択します。

https://developer.mozilla.org/ja/docs/Web/CSS/:where
https://developer.mozilla.org/ja/docs/Web/CSS/:is

なお、:where()は詳細度が常に0になるのに対して、:is() は引数内で最も詳細度の高いセレクターの詳細度になる点が異なります。

複数のセレクタをまとめて指定できて便利、くらいに思われがちな:is():where()ですが、もっと便利な使い方があります。
:is()または:where()を使うことで、これまで「前置修飾」で書かなきゃいけなかったセレクタが「後置修飾」で書けるので、間に割り込む必要がなくなります。

/* 前置修飾で書いていた隣接するときのスタイルを */
.container h2 + p {}
/* :is()で後置修飾にできる! */
.container p:is(h2 + *) {}

同様に、内包するときのスタイルも後置修飾で書けますが、これらは同一ではないことに注意してください。

/* 前置修飾で書いていた隣接するときのスタイルを */
.container section p {}
/* :is()で後置修飾にできるが、同一ではないことに注意 */
.container p:is(section *) {}

状況によっては意図しない要素にスタイルが適用される可能性があります。
以下のHTMLサンプルを参照してください。

<section>
  <div class="container">
    <p>.container p:is(section *) にマッチする</p>
    <section>
      <p>.container p:is(section *) と .container section p にマッチする</p>
    </section>
  </div>
</section>

Sass で特に便利

後置修飾スタイルは、Sass で記述する場合に特に便利です。
前述の通り、前置修飾のセレクタが存在すると p に対するスタイルの記述が 2 つのブロックに分かれてしまいましたが、

// is, where を使わない場合
.container {
  p {
    &:first-child {
    }
  }
  h2 + p {
  }
}

後置修飾にすることで、p に対するスタイルの記述が 1 つのブロックにまとまります。
これにより、 p 要素に対する指定がどのような条件で分岐しているかがコード上でわかりやすくなります。

// is, where を使う場合
.container {
  p {
    &:first-child {
    }
    &:is(h2 + *) {
    }
  }
}

発展

また、あまり推奨しない使い方ですが、次のような使い方もできます。
ulとolのように、「複数の要素に基本共通のスタイルを当てる」「だけどちょっとだけ違う」という場合、
普通以下のように書きますが、

// is, where を使わない場合
ul,
ol {
  // 共通のスタイル
}
ul {
  // ulだけのスタイル
}
ol {
  // olだけのスタイル
}

以下のように書き換えることで、一部条件分岐している、ということをコード上で明示できます。

// is, where を使う場合
:is(ul, ol) {
  // 共通のスタイル
  &:where(ul) {
    // ulだけのスタイル
  }
  &:where(ol) {
    // olだけのスタイル
  }
}

ただし、 :is(ul, ol):where(ol) という、これだけ見るとなぜそんな書き方をしているか意味不明なセレクタが出力されることになるので、濫用しないことをおすすめします。

GitHubで編集を提案

Discussion