React の style タグで ランタイム CSS-in-JS を実装する
モチベーション
React 19 では、スタイルシートに対する組み込みのサポートを提供することでこの複雑さに対処し、またクライアントにおける並行レンダー機能やサーバにおけるストリーミングレンダー機能との深い統合を行います。
https://ja.react.dev/blog/2024/12/05/react-19#support-for-stylesheets
少し前になりますが、React 19 のリリースでスタイルシートのサポートがされ(上記、引用)、それに伴い <style> タグの使用が可能になりました。厳密には 18 から Canary として提供されていました。私が観測していた限りでは SNS でもある程度の盛り上がりを見せていたように記憶しています。
We might not even have to use Panda at all. The progress of the style tag in React 19 is very promising and could give Chakra an even greater performance boost without any migration cost.
https://www.chakra-ui.com/blog/00-announcing-v3#does-chakra-v3-use-panda-internally
私が愛用している Chakra-UI という CSS ライブラリのメジャーバージョンアップデートのリリースノートでも「<style> タグ気になるよねぇ(意訳)」と記載されていました(上記、引用)。
リリースから少し経ったものの、最近、社内の勉強会で <style> タグの内容についておしゃべりしたのですが、少し忙しい時期が重なってしまい踏み込みの浅い内容となっていたため、改めて調べ直したのが本日のモチベーションです。
当時、盛り上がっていた理由
ランタイム CSS-in-JS は RSC(React Sever Component)の登場により下火になっていたように思います。
それは、DOM API によって <style> タグを挿入する方式のため、サーバーサイドでレンダリングする RSC に対応できないという問題があったためでした。そのため、多くのランタイム CSS-in-JS のライブラリは大きな決断を強いられており、あるライブラリはメンテナンスをストップしたり、あるライブラリはゼロランタイム CSS-in-JS への切り替え対応をしたり...
Moving away from runtime CSS-in-JS is on our mid-term roadmap. Like you said, we'll be leveraging most of PandaCSS's core utilities to create a static CSS-based system.
https://github.com/chakra-ui/chakra-ui/discussions/10197#discussioncomment-13924035
ちなみに、ランタイム CSS-inJS である Chakra-UI は具体的な日付は示されていませんが、将来的にはゼロランタイム CSS-in-JS へ切り替えていくようです(上記、参照元)。
そんな中で <style> タグがネイティブサポートされるリリースがあり、CSS-in-JS の新しい実現方式が示され、盛り上がっていたのだと思います。また、推測にはなりますが、Chakra UI のメンテナーは、RSC に対応したランタイム CSS-in-JS という可能性も考慮に入れた上でのリリースノートへの記載内容だったように思います。
React 19 でリリースされた style タグとは?
概要
ブラウザ組み込みの <style> タグが使用できるようになりました(以下、例)。
<style>{` p { color: red; } `}</style>
この機能は他のブログ等では Style Tag Hoisting という呼ばれ方もされており、これは <body> タグに定義されている <style> タグを吸い上げて(=ホイストして)、<head> タグに挿入してくれる 、という機能であるためです。吸い上げる対象は <style> タグに限らず、<title>タグや <link> タグも含まれます。
従来は <head> タグに何かを実装する場合は、ライブラリ側の独自実装を使用することが多く、<head> タグは React を使っているとコントロールしづらいところだったのですが、ネイティブサポートされるようになり、とても扱いやすくなりました。
(詳細1)スタイルの優先度
CSS は重複したスタイルを定義をした場合は後勝ちになります。<style> タグを吸い上げて <head> タグに挿入してくれるのは良いとして、後勝ち仕様だと、ある程度の数のコンポーネントを構築していくとだんだんと扱いきれなくなってきます。以下の例のようなスタイルを定義をした場合は、p に red が適用されます(後勝ち仕様)。
function SampleComponent() {
return (
<div>
<style>{` p { color: blue; } `}</style>
<style>{` p { color: red; } `}</style>
<p>Hello React.</p>
</div>
)
}
そこで <style> タグの吸い上げに止まらず、優先度(precedence)を付与することで、他の <style> との優先度が調整可能になっています。以下のように優先度(precedence)を付与できます。
<style precedence="high" href="high-paragraph">{` p { color: red; } `}</style>
優先度(precedence)を付与することで、以下のような実装の場合でも後勝ち仕様から脱却し、p に blue を適用することができます。
function SampleComponent() {
return (
<div>
<style
href="text-color-a"
precedence="high"
>{` p { color: blue; } `}</style>
<style
href="text-color-b"
precedence="default"
>{` p { color: red; } `}</style>
<p>Hello React.</p>
</div>
)
}
注意点としては、precedence は最初に現れた値を「低優先度」と考え、後に現れたものを「高優先度」と見なします。以下のようにルートに近いコンポーネントで、優先度順に定義してあげるのが良さそうです。
export default function Home() {
return (
<Box>
<style href="base-defautl" precedence="default" />
<style href="high-defautl" precedence="high" />
<SampleComponent />
</Box>
)
}
(詳細2)重複解消処理
以下のようなスタイルを定義して、コンパイルされた場合、<style> タグは2つ生成されそうですが…同じ href を持つスタイルに対して重複解消処理を行います。そのため、SampleComponent を複数回定義していますが、スタイルの定義は重複解消処理の対象になり、コンパイル後はスタイル定義は1つだけになると思います。
function SampleComponent() {
return (
<div>
<style
href="text-color-a"
precedence="high"
>{` p { color: blue; } `}</style>
<p>Hello React.</p>
</div>
)
}
export default function Home() {
return (
<Box>
<SampleComponent />
<SampleComponent />
</Box>
)
}
(詳細3)レンダリング周り
サーバサイドレンダリングの場合、React は <head> タグにスタイルシートを含めることで、それをブラウザがロードするまで描画が起きないことを保証してくれます。また、クライアントサイドレンダリングの場合、<link> タグに限った話ではありますが、<link> タグをレンダーしているコンポーネントは、スタイルシートが読み込まれている間はサスペンドしてくれます。
コンポーネントがレンダリングされてからスタイルが適用されると、後から色が変わってしまうような挙動になってしまいますが、そのような事象は起きないように React 側で制御されています。
ここまでをまとめると、スタイルの優先度・重複解消処理・レンダリングでの扱いなど…React で CSS を扱う上で助かる機能が 19 で多くリリースされたことがわかります。
style タグで ランタイム CSS-in-JS を実装する
シンプルな実装
<style> タグを使って簡易なランタイム CSS-in-JS を実装してみようと思います。以下のサンプル実装に対して、<style> タグを使用して、スタイルを適用してみます。
export default function Home() {
return (
<div>
<p>Hello React.</p>
</div>
)
}
CSS-in-JS にしたいので、オブジェクト形式でスタイルを定義します。また、定義したスタイルを <p> に必ず適用したいため、優先度(precedence)を high にします。
function HightPrecedenceStyle() {
const style = css({
p: {
color: "blue",
fontSize: "16px"
},
});
return (
<style precedence="high" href="high">{style}</style>
)
}
css 関数はオブジェクト形式をインラインスタイル方式に書き換える関数です。本筋ではないので、コードは貼っておきますが、読んで頂く必要はありません。
css 関数の実装
function css(styles: object) {
let cssString = '';
for (const [selector, rules] of Object.entries(styles)) {
cssString += `${selector} {\n`;
for (const [property, value] of Object.entries(rules)) {
const cssProperty = property.replace(/([A-Z])/g, '-$1').toLowerCase();
cssString += ` ${cssProperty}: ${value};\n`;
}
cssString += '}\n';
}
return cssString;
}
HightPrecedenceStyle をサンプル実装に追加します。とてもシンプルな実装ですが、DOM API を使用せずにスタイルを適用し、RSC としてレンダリングされます。
export default function Home() {
return (
<div>
<p>Hello React.</p>
<HightPrecedenceStyle />
</div>
)
}
優先度を考慮する
せっかくなので優先度(precedence)も使って実装してみます。HightPrecedenceStyle とほぼ同じコンポーネントですが、優先度(precedence)に low を設定し、フォントカラーを赤色にしました。
function LowPrecedenceStyle() {
const style = css({
p: {
color: "red",
fontSize: "16px"
},
});
return (
<style precedence="low" href="low">{style}</style>
)
}
そして、LowPrecedenceStyle をサンプル実装に追加しました。CSS は重複したスタイルを定義をした場合は後勝ちになります。しかし、優先度(precedence)を振っているため、high のスタイルが適用されます。ちなみに、優先度(precedence)を React に伝えるために冒頭に優先度の定義をしています。
export default function Home() {
return (
<div>
<style precedence="low" href="baseLow" />
<style precedence="high" href="baseHigh" />
<p>Hello React.</p>
<HightPrecedenceStyle />
<LowPrecedenceStyle />
</div>
)
}
シンプルな実装でしたが、style タグで ランタイム CSS-in-JS を実装できたと思います。ただ、ライブラリとして使用するには当然ながら考慮不足だらけです。クライアントサイドでの再レンダリングの考慮はしていないですし、スタイルの重複問題を解消するためには、ハッシュ化されたクラスを生成したりするアプローチの方が良さそうに思います。
CSS-in-JS のライブラリにどう組み込まれる?
Restyle という新しいライブラリの登場
<style> タグを使用した Restyle というライブラリがあります。先ほど、<style> タグを使用したシンプルな CSS-in-JS のシンプルな実装例は示したところですが、ライブラリでどのように <style> タグが使用されているかも含めて見ていこうと思います。
また、あくまでも個人的な意見ですが、現時点では中~大規模なアプリケーションに耐えられる機能は具備していないように思うので、本番環境にデプロイするアプリケーションに適用する場合は慎重に判断された方が良いかと思います。

Is this going to be maintained?
https://github.com/souporserious/restyle/issues/34
メンテナーの方は今後も継続して開発をしていく意向の様子なので(上記、参照元)、気になる方は追ってみてもいいかもしれないです。
Restyle のライブラリの概要
Works in Server and Client Components
Compatible with Suspense and streaming
https://github.com/souporserious/restyle
<style> タグを使用している、ということは既に書いた通りですが、Restyle のライブラリの概要としてはサーバーコンポーネントに対応したランタイム CSS-in-JS だと思います(上記、参照元)。色々と特徴はありますが、個人的にはこれが一番大きい(他ライブラリとの差異)だと思います。
2.2kb minified and gzipped
また、もう1点気になる点としてはサイズが非常に小さいです。現時点では機能が少ないため、単純に他の CSS ライブラリとの比較はフェアではないかと思いますが、これまで CSS ライブラリ側で実装していたようなことをネイティブサポートにのっかっている分、同じ機能群になってもサイズは小さくなるのだろうと思います。
どのように style タグが使用されているか
High > Medium > Low という優先度がスタイルのプロパティ毎に定義されています。
※ グローバルの CSS のスタイルなど…他にも専用の優先度は定義されていましたが、枝葉なので割愛です
const precedence = l.has(key) ? 'l' : m.has(key) ? 'm' : 'h'
const className = precedence + hash(key + value + selector + atRules.join(''))
let rule = createRule(className, selector, key, value)
// REF: https://github.com/souporserious/restyle/blob/b8ce746c3f9a1178310135155b0487b45dc2f815/src/create-rules.ts#L82-L85
具体としては以下のように Low と Medium のみが定義されており、これらに含まれないスタイルは High が適用されます。
/** Low precedence CSS styles. */
export const l = new Set([
...省略
'border',
...省略
])
/** Medium precedence CSS styles. */
export const m = new Set([
...省略
'borderLeft',
'borderRight',
'borderTop',
'borderBottom',
...省略
])
// REF: https://github.com/souporserious/restyle/blob/b8ce746c3f9a1178310135155b0487b45dc2f815/src/utils.ts
以下のような定義をした場合に上から High, Medium, Low の優先度が割り振られると思うのですが、より個別のプロパティに対して優先度が高くなるように設計されているようです。
import { styled } from 'restyle'
const PrecedenceExample = styled('div', {
borderLeftWidth: '3px', // High
borderLeft: '2px solid blue', // Medium
border: '1px solid red', // Low
})
また、優先度毎(levels)に href を定義して重複解消処理をしている様子です。
<style
href={`${levels.low}i`}
precedence={levels.low}
{...sharedProps}
/>
// REF: https://github.com/souporserious/restyle/blob/4352af57a9b2148a41a092bffe3173090110378c/src/client-styles.tsx#L54-L58
まとめ
style タグの概要、style タグのサンプル実装、Restyle での Style タグの使われ方についてサラッとにはなりますが見てみました。間違ったことが記載されていたら優しくコメント頂けますと幸いです。
参考
Discussion