[CSS] :is(), :not(), :where(), :has() の意外な使い方

CSS の擬似クラスである :is(), :not(), :where(), :has() はとても便利ですよね。CSS をある程度使いこなせている人なら、使ったことがないという人はいないのではないでしょうか?

今回は、これらの便利な擬似クラスで、こんなこともできるんだと驚いた、少し変わった使い方をシェアしたいと思います。

:first-child 擬似クラスの代替セレクタ

/* p:where(:first-child) {} */
p:not(* + *) {}

:not() 擬似クラスとフクロウセレクタ (* + *) を組み合わせ、前に兄弟要素があることを否定することで、:first-child 擬似クラスを使用した場合と同様の要素を選択することができます。

:last-child 擬似クラスの代替セレクタ

/* p:where(:last-child) {} */
p:not(:has(+ *)) {}

:has() 擬似クラスと次兄弟セレクタ (+ *) に :not() 擬似クラスを組み合わせ、後に兄弟要素があることを否定することで、:last-child 擬似クラスを使用した場合と同様の要素を選択することができます。

:only-child 擬似クラスの代替セレクタ

/* p:where(:first-child:last-child) {} = p:where(:only-child) {} */
p:not(* + *, :has(+ *)) {}

:first-child, :last-child 擬似クラスの代替ができているので、:first-child であり :last-child でもある :only-child 擬似クラスの代替もできます。

:first-child 擬似クラスの否定セレクタ

/* p:not(:where(:first-child)) {} = p:where(:nth-child(n + 2)) {} */
p:is(* + *) {}

:is() 擬似クラスとフクロウセレクタを組み合わせることで、:first-child ではない要素を選択することができます。

:last-child 擬似クラスの否定セレクタ

/* p:not(:where(:last-child)) {} = p:where(:nth-last-child(n + 2)) {} */
p:has(+ *) {}

:has() 擬似クラスと次兄弟セレクタを組み合わせることで、:last-child ではない要素を選択することができます。

:first-child, :last-child 擬似クラスの否定セレクタ

/* p:not(:where(:first-child, :last-child)) {} = p:where(:nth-child(n + 2):nth-last-child(n + 2)) */
p:is(* + *):has(+ *) {}

:is() 擬似クラスとフクロウセレクタ、:has() 擬似クラスと次兄弟セレクタを組み合わせることで、:first-child でも :last-child でもない要素を選択することができます。

代替・否定セレクタと入れ子セレクタと組み合わせる

:is(), :not(), :where(), :has() 擬似クラスには、入れ子セレクタ (&) を使うことができます。入れ子セレクタを使うことで、コード量が多くなりがちなセレクタをコンパクトに記述することができます。

下記の CSS コードは見出しの位置で変える段落の設定例です。

:where(h1, h2, h3, h4, h5, h6) {
  /* p:is(:where(h1, h2, h3, h4, h5, h6) + *) = 前に見出しがある段落 */
  p:is(& + *) {}

  /* p:has(+ :where(h1, h2, h3, h4, h5, h6)) = 後に見出しがある段落 */
  p:has(+ &) {}

  /* p:is(:where(h1, h2, h3, h4, h5, h6) + *):has(+ :where(h1, h2, h3, h4, h5, h6)) = 前後に見出しがある段落 */
  p:is(& + *):has(+ &) {}

  /* p:not(:where(h1, h2, h3, h4, h5, h6) + *) = 前に見出しがない段落 */
  p:not(& + *) {}

  /* p:not(:has(+ :where(h1, h2, h3, h4, h5, h6))) = 後に見出しがない段落 */
  p:not(:has(+ &)) {}

  /* p:not(:where(h1, h2, h3, h4, h5, h6) + *, :has(+ :where(h1, h2, h3, h4, h5, h6))) = 前後に見出しがない段落 */
  p:not(& + *, :has(+ &)) {}
}

マージンの一括設定に応用した例

マージン (余白) の設定は、ウェブサイト制作者毎の考え方の違いが顕著に表れる部分かと思います。個人的には、デフォルトスタイルのリセット時にマージンを一括設定して、ウェブページの部分毎に微調整するという手法を採用しています。

そのため、リセット時の段階でかなり詳細にマージンの設定を行うことになります。例えば、次のようなデザインルールでマージンを設定します。

  • マージンを設定しておきたい要素 (以下、余白設定要素) は、article, section, nav, aside, h1, h2, h3, h4, h5, h6, hgroup, header, footer, address, p, hr, pre, blockquote, ol, ul, menu, dl, figure, main, search, div:where(.has-margin-block), table, form, fieldset, details である。
  • 余白設定要素の次の余白設定要素にのみ margin-block-start プロパティを設定する。
  • 余白設定要素のうち、article, section, nav, aside (以下、セクショニングコンテンツ) や h1, h2, h3, h4, h5, h6, hgroup (以下、ヘディングコンテンツ) などは、その他の余白設定要素に比べ、広めにマージンを設定する。
  • セクショニングコンテンツなどは後続する要素との区切りを明確にするため、広めの margin-block-end プロパティを設定する。
  • article, nav, aside, header, footer, blockquote, ol, ul, menu, dl, figure, table, fieldset, details といった子孫要素に任意のブロックレベル要素を持つことができる要素 (以下、グルーピング要素) の中にある余白設定要素は、通常よりも狭めに margin-block-start プロパティと margin-block-end プロパティを設定する。

実際の CSS コードは次のようになります。

/* マージンの値となる変数を設定 */
html {
  --size-rem_xx-small: calc(var(--size-rem_medium) / 8);
  --size-rem_x-small: calc(var(--size-rem_medium) / 4);
  --size-rem_small: calc(var(--size-rem_medium) / 2);
  --size-rem_medium: 2rem;
  --size-rem_large: calc(var(--size-rem_medium) * 2);
  --size-rem_x-large: calc(var(--size-rem_medium) * 4);
  --size-rem_xx-large: calc(var(--size-rem_medium) * 8);
}

/* デフォルトスタイルのマージンをリセット */
body, h1, h2, h3, h4, h5, h6, address, p, hr, pre, blockquote, ol, ul, menu, li, dl, dd, figure, table, form, fieldset, input, button, select, textarea, progress, meter {
  margin: 0;
}

/* 余白設定要素を列挙 */
:where(article, section, nav, aside, h1, h2, h3, h4, h5, h6, hgroup, header, footer, address, p, hr, pre, blockquote, ol, ul, menu, dl, figure, main, search, div:where(.has-margin-block), table, form, fieldset, details) {
  /* 通常のマージン設定 */
  :is(address, p, pre, blockquote, ol, ul, menu, dl, figure, search, div:where(.has-margin-block), table, form, fieldset, details):is(& + *) {
    margin-block-start: var(--size-rem_medium);
  }

  /* 広めのマージン設定 */
  :is(article, section, nav, aside, h1, h2, h3, h4, h5, h6, hgroup, header, footer, hr, main):is(& + *) {
    margin-block-start: var(--size-rem_large);
  }
  :is(article, section, nav, aside, header, footer, hr, main):has(+ &) {
    margin-block-end: var(--size-rem_large);
  }

  /* グルーピング要素を列挙 */
  :where(article, nav, aside, header, footer, blockquote, ol, ul, menu, dl, figure, table, fieldset, details) & {
    /* 通常のマージン設定 */
    :is(address, p, pre, blockquote, ol, ul, menu, dl, figure, search, div:where(.has-margin-block), table, form, fieldset, details):is(& + *) {
      margin-block-start: var(--size-rem_small);
    }

    /* 広めのマージン設定 */
    :is(article, section, nav, aside, h1, h2, h3, h4, h5, h6, hgroup, header, footer, hr, main):is(& + *) {
      margin-block-start: var(--size-rem_medium);
    }
    :is(article, section, nav, aside, header, footer, hr, main):has(+ &) {
      margin-block-end: var(--size-rem_medium);
    }
  }
}

使いどころが限られている感は否めませんが、意外なところで役に立つかもしれませんね。疑問点や応用例などがあれば気軽にコメントしてください。

関連記事