ReactのCSS Modulesでグローバル汚染する問題は@scopeで防げる

CSS ModulesでHTMLタグを使用した際の問題

React開発でおなじみのCSS Modulesとはコンポーネントごとにクラス名をカプセル化してくれる便利な仕組みです。

しかし、CSS Modulesはh1やinputなどのHTML要素(要素セレクタ)を直接書くと、スコープが効かずにグローバル汚染を引き起こすという問題があります。

例として、以下のようにh1タグで指定した場合は、About.tsxなどのほかのコンポーネント(ページ)にも適用されてしまいます。

Home.module.css
h1 {
  color: green;
}
CSS ModulesでHTML要素を直接指定するとグローバル汚染が発生する

また、CSS Modulesだとclass名の指定が「className={styles.sample} 」のようになり、「className="sample"」のように直感的に書けないので手間がかかり、可読性も良くないです。

Home.tsx
import styles from './Home.module.css'

function Home() {
  return (
    <div className="RootHome">
      <h1>Home</h1>
      <p className={styles.sample}>sample</p>
    </div>
  )
}

export default Home

@scopeでコンポーネント内だけに適用する方法

前述の問題はコンポーネント内に@scopeで範囲指定するためのCSSのclass名を付与して、@scope内に従来通りのCSSのコードを書くことで解消できます。

まず、コンポーネントに `Root${scopeName}` のCSSのclassを付与するための関数を以下のように作成します。

ScopeClass.tsx
import type { ComponentType } from 'react'

export function scopeClass<P extends object>(Component: ComponentType<P>, scopeName: string) {
  const scopeClassName = `Root${scopeName}`

  return function WrappedComponent(props: P) {
    return (
      <div className={scopeClassName}>
        <Component {...props} />
      </div>
    )
  }
}

Component.displayNameを使うと関数名がビルド時に難読化(minify)により名前が変わるので、引数から文字列で指定します。

次にHome.tsxやAbout.tsxなどのコンポーネントでscopeClassの関数を読み込んで、「export default scopeClass(Home, 'Home')」にしてコンポーネントを返します。

こうすることで「<div className="RootHome">」のようなclass名を付与した状態で返します。

Home.tsx
import { scopeClass } from './ScopeClass'
import './Home.css'

function Home() {
  return (
    <>
      <h1>Home</h1>
      <p className="sample">sample</p>
    </>
  )
}

export default scopeClass(Home, 'Home');
About.tsx
import { scopeClass } from './ScopeClass'
import './About.css'

function About() {return (
    <>
      <h1>About</h1>
      <p className="sample">sample</p>
    </>
  )
}

export default scopeClass(About, 'About');

あとはCSSで最初に「@scope (.RootHome) { }」を書いて、その中に記述すれば該当のコンポーネントだけに適用されます。

CSS Modulesではなくなるので、ファイル名はHome.cssのようにシンプルにできます。

Home.css
@scope (.RootHome) {
  h1 {
    color: green;
  }

  .sample {
    color: pink;
  }
}

About.cssなどのほかのコンポーネントのCSSも同様に@scopeで範囲を指定して書きます。

About.css
@scope (.RootAbout) {
  h1 {
    color: blue;
  }
}

ReactでCSS Modulesではなく@scopeを使用したサンプル

まとめ

CSS Modulesはクラス名の衝突を防げる便利な仕組みですが、h1やinputなどのHTML要素を直接指定するとグローバル汚染が発生するという問題があります。

一方、@scopeを使用すれば、コンポーネント単位でCSSの適用範囲を制限しつつ、従来のシンプルなCSSに近い書き方が可能で、Home.cssのような分かりやすいファイル名にできるメリットもあります。

ReactでCSSのスコープ管理を行いたい場合はCSS Modulesだけでなく、@scopeを利用した構成も選択肢の1つとして検討すると良いでしょう。