タグアーカイブ DOM操作

::nth-letter を今すぐ使う Shim の仕組み

::nth-letter を今すぐ使う Shim の仕組み

::nth-letter というCSSのセレクタは正式な仕様には存在しない。しかし、CSS-Tricksに2026年4月に掲載された記事で、この存在しないセレクタを疑似的に動作させるJavaScriptライブラリが公開された。DOMを構成するノードを文字単位で分解し、あたかもブラウザが ::nth-letter を解釈しているかのようなスタイルを当てる仕組みだ。

文字ごとに異なる色や変形を施すタイポグラフィを、面倒なHTMLのマークアップから解放する可能性を秘めている。この記事では、::nth-letter Shimのアイデアと実装、そして限界を詳しく見ていく。

::nth-letter で実現できること

::nth-letter で実現できること

CSSには、段落の最初の文字だけを装飾する ::first-letter 疑似要素がある。ドロップキャップの表現などに使われてきた。もし ::nth-letter が存在すれば、::first-letter の一般化として、任意の位置の文字を直接スタイリングできる。例えば、見出しの中で奇数番目の文字と偶数番目の文字を左右に傾けてレンガ模様のような演出を施せる。

記事の著者が提示した ::nth-letter(even) を用いたCSSの例が次のパターンだ。

h1.fancy::nth-letter(n) {
  display: inline-block;
  padding: 20px 10px;
  color: white;
}
h1.fancy::nth-letter(even) {
  transform: skewY(15deg);
  background: #C97A7A;
}
h1.fancy::nth-letter(odd) {
  transform: skewY(-15deg);
  background: #8B3F3F;
}

このコードを疑似的に再現したデモが以下だ。実際には ::nth-letter は使えないが、Shimを通じてスタイルが適用された状態を静的に示している。

Rainbow!

このデモでは「Rainbow!」の各文字が奇数・偶数で交互に傾きと背景色が切り替わる。コード上では <h1 class="fancy">Rainbow!</h1> という1つの見出しに、::nth-letter の指定だけで実現できるイメージだ。

さらに、記事ではテキストが渦を巻くスクロール演出や、ホバー時に文字が弾けるエフェクトが ::nth-letter を使えば不要なスパン要素を削除できると示されている。現実には、これらのデモはすべてJavaScriptでDOMを文字単位に分解して作動している。

なぜ ::nth-letter は存在しないのか

なぜ ::nth-letter は存在しないのか

「n番目」の解釈が定まらない

CSSセレクタの世界で「n番目」とは何か。例えば <p>AB<span>CD</span>EF</p> というHTMLで3番目の文字を指す場合、単純に文字の出現順(視覚的な順序)ならC。DOMの子要素単位で数えるならE。さらにCSSで右から左へ読む表記方向が指定されていれば、結果は変わる。どの基準を採用するかで実装が大きく異なるため、標準化が難しい。

「文字」の定義が言語ごとに異なる

ウェブの半分は英語以外の言語で構成されている。多くの言語では1つの文字を複数の符号で表す(合字など)ため、「letter」の区切りが曖昧だ。::first-letter ですらブラウザ間で挙動に差異がある。厳密に「letter」を定義しようとすると、あらゆる言語を考慮しなければならず、実装コストが跳ね上がる。

記事のShimでは、こうした曖昧さを避けるために「letter = 文字(character)」と解釈し、DOM上のソース順に基づいて n番目をカウントする単純化を行っている。この割り切りが、実用的なShimを短期間で作る鍵になった。

JavaScript Shim による ::nth-letter の実装方法

JavaScript Shim による ::nth-letter の実装方法

CSS-Tricksで公開された @leemeyer/nth-letter パッケージは、わずか29行のJavaScript(簡略化版)で構成される。その仕組みは大きく3段階に分かれる。

無効なCSSを正規表現で書き換える

まず get-css-data ライブラリを使って、ページ内のすべてのCSS(<style> タグと外部スタイルシート)を文字列として取得する。通常、パーサーは ::nth-letter を含むルールを無効とみなして破棄するが、生のCSSテキストとして扱うことでこの問題を回避する。

次に正規表現で ::nth-letter(...) を検索し、.char:nth-child(...) に置換する。たとえば、

.rainbow::nth-letter(2n) { color: #f432a0; }

は、

.rainbow .char:nth-child(2n) { color: #f432a0; }

に変換される。こうして得られた有効なCSSを新しい <style> タグでページに挿入する。元の無効なスタイルは削除する。

DOMを文字単位に分割する

変換後のCSSは .char クラスを持つ子要素を対象としている。そのため、Shimは対象となる要素内のテキストを1文字ずつ <div class="char"> に分解する必要がある。ここで、アニメーションライブラリGSAPの SplitText プラグインを使用する。このプラグインは自動的にテキストを分解し、視覚的な文字列とスクリーンリーダー向けのアクセシビリティ属性を埋め込む。

具体的な処理は以下のコードに集約される。

selectors.forEach(selector => {
  document.querySelectorAll(selector).forEach(el => {
    if (el.hasAttribute('data-nth-letter')) return;
    el.setAttribute('data-nth-letter', 'attached');
    new SplitText(el, { type: 'chars', charsClass: 'char' });
  });
});

これにより、ページ読み込み時にターゲット要素が自動的に文字単位のDOMツリーへ展開され、あらかじめ書き換えておいたCSSルールが文字単位で適用される。

正真正銘のポリフィルではない

仕様が存在しないため、このライブラリはポリフィル(Polyfill)ではなくShim(シミュレーター)に分類される。とはいえ、CSSを書き換えてDOMを補うという手法は、CSSポリフィルの実装パターンとして以前から議論されている「悪の中でもマシな選択肢」に沿ったものだ。

Shadow DOM を使ったアプローチとその問題点

Shadow DOM を使ったアプローチとその問題点

先の実装では、文字ごとに <div class="char"> がDOM上に追加される。これがマークアップを汚染し、他のJavaScriptやスタイルに影響を与える懸念がある。そこで記事の著者は、文字要素をShadow DOM内に隠蔽する改良版を試作した。

Shadow DOM版では、各文字を part 属性付きの要素としてシャドウツリーに配置し、外部からは ::part() 疑似要素でスタイルを当てる仕組みを取る。これにより、通常のDOM(Light DOM)は汚染されない。しかし、次の大きな制約が明らかになった。

  • Shadow DOMをアタッチできない要素<a><p> など、一部のHTML要素はShadow Rootを保持できず、見出しやテキストに多用されるタグが使えない。
  • 構造疑似クラスとの相性::part() 擬似要素と :nth-child() を組み合わせられない制限がある。そのため、sibling-index() など高度なCSS関数を用いたスタイルが不可能になる。

結局、記事の著者は「DOMが汚れても、実用上はLight DOMを分割するバージョンが優れている」と結論づけた。アクセシビリティの面でも、GSAPの SplitTextaria-hiddenaria-label を自動付与するため、スクリーンリーダーへの配慮はある程度カバーされている。

::nth-letter Shim の限界と実用上の注意点

::nth-letter Shim の限界と実用上の注意点
  • 動的なDOM変更に未対応。ページ読み込み後にDOMやスタイルが動的に変わった場合、Shimが処理をやり直すことはない。MutationObserverで追従は可能だが未実装。
  • CORSの壁。外部スタイルシートがCORSポリシーで読み取り不可の場合、中の ::nth-letter ルールは変換されず無効のまま残存する。
  • CSS全置換のリスク。正規表現による一括変換が思わぬセレクタの誤変換を引き起こす恐れがある。大規模サイトでは注意が必要。
  • パフォーマンス。大量のテキストを文字単位に分解するとDOMノード数が爆発的に増え、レンダリング負荷が上がる。
  • スクリーンリーダーの断片化。GSAPの自動付与でも、全ての環境で文字ごとの読み上げが完全に抑制される保証はない。

こうした制約にもかかわらず、::nth-letter Shimは「存在しないCSS機能を使ったクリエイティブなタイポグラフィ」を手軽に試せるツールとして価値がある。もし将来ブラウザがネイティブ対応すれば、CSS部分はそのまま残し、ライブラリへの参照を外すだけで移行できる設計だ。

この記事のポイント

  • ::nth-letter は存在しないが、JavaScriptによるShimで疑似的に利用できる。
  • DOMを文字単位に分割し、:nth-child へ変換する手法で実現。GSAP SplitTextが鍵。
  • 「n番目」「文字」の定義の曖昧さが標準化を阻んでいる。
  • Shadow DOM版ではLight DOMを保護できるが、使えないタグや機能制限が多い。
  • 実用にはCORSやパフォーマンス、アクセシビリティの注意が必要。