CSS4セレクタ (Selectors Level 4) の新機能

2011年9月29日、CSS3セレクタ仕様の勧告と同時に、Selectors Level 4のFPWDが公開されました。Level 4では誰もが考えた「親セレクタ」をはじめ、より高機能なセレクタが提案されています。このノートでは、Selectors Level 4仕様で新しく追加された機能について紹介します。

公式な草案にリンクしている部分はあるものの、このノートが参照する仕様書はEditor's Draftの情報になります。このため、WDと記述が食い違っている可能性があります。

まだ書いている途中だったりします。このノートの変更はGitHubのレポジトリからたどってください。

Selectors Level 4仕様について

Selectors Level 4 は、Selectors Level 3仕様の上位にある新しいセレクタ仕様です。Level 3では:nth-child()など便利なセレクタが数多く追加されましたが、Level 4仕様ではより便利なセレクタや、新しい仕組みが検討されています。

仕様が初期の段階ですから、現在の仕様書にある機能がそのうちなくなる可能性があります。反対に、現在の仕様書にない新しいセレクタが追加される可能性もあります。

セレクタの構造と用語

ふだん「セレクタ」と読んでいるものですが、仕様書ではその種類や用語が細かく定義されています。

セレクタ
単体セレクタ、合成セレクタ、複合セレクタ、セレクタリストを総称
単体セレクタ (simple selector)
型セレクタ (要素型セレクタ、type selector)、ユニバーサルセレクタ、属性セレクタ、クラスセレクタ、IDセレクタ、擬似クラス
合成セレクタ (compound selector)
結合子をはさまない単体セレクタの連なり
複合セレクタ (complex selector)
合成セレクタが結合子によって連なったもの
結合子 (combinator)
合成セレクタ同士の関係を表すもの、空白 ( )、大なり (>)、プラス記号 (+)、チルダ (~) がある
(日本語では俗にそれぞれ「子孫セレクタ」「子セレクタ」「隣接セレクタ」「兄弟セレクタ」などと呼ばれる)

これらを厳密に区別して使う機会はあまりないと思いますが、覚えておくと仕様書を読むときに便利です。

セレクタの対象と?selector記法

FPWDが出てすぐ、$から?になりました。(参照)

CSS4セレクタでは、念願かなって要素の「親」を指定できるようになりました。
ただし、「親セレクタ」が導入されたわけではありません。むしろ、もっと便利な機能の、ひとつの応用例として使えるようになっています。

とりあえず、何かの親にスタイルを当ててみましょう。

ul > ?li > a { list-style: none; }

liの前にさりげなく?とあるのがわかるでしょうか。これがCSS4で導入された新機能です。

セレクタによって表される要素 (スタイルが適用される要素) を、セレクタ仕様では「対象 (subject)」と呼びます。これまでのレベルでは、各セレクタのうち最後の要素が対象となっていました。

CSS4では、対象としたい合成セレクタの前にドル記号(?)をつけることで、この対象を変更できるようになりました。ですので、ul > ?li > aとすると、「子にaを持つli要素」にスタイルを当てられるわけです。

「どれか」にマッチする:matches()

CSS4では、:matches()という擬似クラスが導入されました。引数に複数のセレクタを書くと「そのうちのどれか」にマッチします。

カンマと何が違うんだという話ですが、ネストした場合にとても楽です。HTML5のsectioning content (section, article, aside, nav)とh1の組み合わせを考えてみましょう。

/* トップレベルの見出し */
h1 {
  font-size: 240%;
  font-weight: bold;
}
/* セカンドレベルの見出し */
section h1, article h1, aside h1, nav h1 {
  font-size: 210%;
}
/* サードレベルの見出し */
section section h1, section article h1, section nav h1, section aside h1,
article section h1, article article h1, article nav h1, article aside h1,
aside section h1, aside article h1, aside nav h1, aside aside h1,
nav section h1, nav article h1, nav nav h1, nav aside h1 {
  font-size: 180%;
}
/* もういや…… */

ひどくなります。aside中にarticleとか、nav中にnavとかどれだけあるんだよと思いもしますが、それはさておいて。ここで:matches()が力を発揮します。

/* トップレベルの見出し */
h1 {
  font-size: 240%;
  font-weight: bold;
}
/* セカンドレベルの見出し */
:matches(section, article, aside, nav) h1 {
  font-size: 210%;
}
/* サードレベルの見出し */
:matches(section, article, aside, nav)
:matches(section, article, aside, nav) h1 {
  font-size: 180%;
}
/* 4th... */
:matches(section, article, aside, nav)
:matches(section, article, aside, nav)
:matches(section, article, aside, nav) h1 {
  font-size: 150%;
}
/* こんな感じで続く! */

こんな風に、かなり見通しが良くなります。

:matches()はもともと、MozillaがGeckoに導入した:-moz-any()という独自拡張のセレクタが元になっています。このセレクタによって、ul, ol, dirなどの複雑なネストや、上記のようなHTML5のセクションと見出しのスタイル付けがとても見やすいものになりました。

:-moz-any()はその後WebKitにも:-webkit-any()として導入され、UAスタイルシートでHTML5のセクションと見出しのスタイルづけに利用されています。

なお、CSS4では引数内のセレクタに結合子が使えないという制限があります。また、:matches():not()をネストさせることはできないともされています。

todo: もっと実用的な例がないか探す。

:not()の引数とセレクタリスト

CSS3の:not()は、ひとつのセレクタしか指定できませんでした。また、擬似クラスや結合子を指定することもできませんでした。CSS4の:not()では、:matches()に合わせ、引数に複数のセレクタをとれるようになりました。また、結合子は使えないものの、擬似クラスの利用が可能になっています。

大文字小文字を問わない属性セレクタ

ごくたまに、セレクタでハマることがあります。属性値が大文字と小文字の混在なのに、セレクタは全部小文字で書いていて適用されなかったケースです。

HTMLの定義では、セレクタはいくつかの属性を除いて、基本的に属性値の大文字小文字を区別します。classidなどは、大文字小文字を間違えるとマッチしないわけです (Quirksモードを除く)。

ただ、大文字小文字を含めてマッチさせたいケースがあるかもしれません。Selectors 4では、属性セレクタの書式が拡張され、大文字小文字を区別しない属性値のマッチングが可能となりました。

elem[attr="whatevercase" i] { ... }

属性セレクタの閉じ括弧の前にiと記述すると、属性値のマッチングで大文字と小文字を問わないようになります。この場合はattr="whatevercase"でもattr="WHATEVERCASE"でもattr="whateverCase"でもマッチします。

なお、ここでの「大文字と小文字」はASCIIの範囲 ([a-z][A-Z]) なので、その範囲外は対象外となります。

要素以外の状態にもマッチさせられる:nth-match()

:nth-child():nth-of-type()は強力な擬似クラスですが、これらは要素にしかかかりません。「class=fooが与えられた<li>のうち3番目」を選択しようとli.foo:nth-child(3)なんて書いてもマッチしませんし、マッチしたとしても間違っています。これは、「3番目の<li>要素がclass=fooを持っているか」という条件を示しているからです。

そんな小難しいことはおいといて、やっぱり要素以外でももっと細かい条件を指定したい場合があります。Selectors 4では:nth-match():nth-last-match()という擬似クラスが導入され、これに対応しました。

構文は:nth-match(an+b of selector-list)となっています。

/* 3番目のli.fooにスタイルをつける */
li:nth-match(3 of .foo) {
  ...
}

関数表記なのにやたらと記述的で面白いですね。

Selectors API 2でコンテキストを記す:scope

セレクタ仕様はCSSだけに限定されないと最初に書きましたが、この:scope擬似クラスはそれをよく示す例で、今のところSelectors API Level 2での利用のみが仕様書に記されています。

Selectors API (Level 1)では、ある要素を基点に、その子孫要素を取得することができなかったのです。さらに、querySelector()querySelectorAll()は、メソッドの呼び出し元がElementであったとしても、セレクタの評価はdocumentからと、理解しがたい仕様でもありました。

<ul>
  <li><a>text</a>
</ul>
<script>
  var li = document.querySelector('li');
  var re = li.querySelectorAll('ul a');
  console.log(re) // <a>が返ってくる
</script>

これについて、Selectors API Level 2では、querySelector(), querySelectorAll()から文脈を限定したマッチングを行えるようにするために:scopeという擬似クラスを導入しました。Selectors Level 4で定義されている:scopeは、これが移管されたものです。

// jQuery
$(elem).find('.section .box')
// Selectors API Level 2
elem.querySelectorAll(':scope .section .box')

まだ紹介してない新機能