【Vue.js】CSS Modulesで書き始めるときに見るべきTips

2019/06 に[【Vue.js】Scoped CSSよりCSS Modulesの方がベターだった件](https://qiita.com/mascii/items/3202b9e18fd4a7366ac1) の記事を書いてから1年が経ちました。
この1年間でよくあった質問の回答や、少し進んだ使い方を紹介したいと思います。

# よくある質問
## `$style.` と書くのは面倒ではないか?
```vue:ExampleComponent.vue
<template>
  <div :class="$style.container">
    ExampleComponent!
  </div>
</template>
```

CSS Modules では親コンポーネントと子コンポーネントの間でクラス名の重複を気にする必要がない[^1]ので、Scoped CSS に比べて**あまり頭を使わず**に**短いクラス名**を付けられます。
この2つのメリットが大きいので `$style.` を付くことで可読性が下がったり、書きづらかったりといったことは感じられませんでした。 

むしろ注意が必要なのは、 `:class` で複数のクラスを当てたい場合は `:class="[$style.class_name_1, $style.class_name_2]"` のように配列構文を用いる必要があることです[^2]。

:bulb: ポイントは3つです:

- 親コンポーネントと子コンポーネントの間で、クラス名の重複を気にする必要はない
- `:class` で複数のクラスを当てるときは配列構文を用いる必要がある
- snake_case でクラス名を定義するのがおすすめ[^3]

## テンプレートで `class` と `:class` は併記できる?
以下のコードのように [Font Awesome](https://fontawesome.com/) などのクラスを `class` で、 `<style module>` で定義したクラスを `:class` で当てることができます:

```vue:FontAwesomeExample.vue
<template>
  <i class="far fa-lightbulb" :class="$style.light"></i>
</template>
```

## CSS Modules で scss は使える?
Vue CLI 環境では [sass](https://www.npmjs.com/package/sass) と [sass-loader](https://www.npmjs.com/package/sass-loader) が導入されていれば scss が使えます。
`<style lang="scss" module>` のようにスタイルブロックで `lang="scss"` を指定するようにします:

```vue
<style lang="scss" module>
.hoge {
  > .fuga {
    /* ... */
  }
  > .piyo {
    /* ... */
  }
}
</style>
```

## Scoped CSS と CSS Modules は共存できる?
ひとつのプロジェクトの中で Scoped CSS と CSS Modules は**共存できます**。
(さらには、ひとつの `.vue` ファイルの中で `<style scoped>` と `<style module>` は共存できます)

実例として、Nuxt.js を用いて実装されている[東京都 新型コロナウイルス感染症対策サイト](https://stopcovid19.metro.tokyo.lg.jp/)(2023/05 公開終了)では、Scoped CSS が使われているコンポーネント(:mag:[GitHubで確認](https://github.com/Tokyo-Metro-Gov/covid19/search?l=Vue&q=scoped)) と CSS Modules が使われているコンポーネント(:mag:[GitHubで確認](https://github.com/Tokyo-Metro-Gov/covid19/search?l=Vue&q=module))が混在していました。

利用頻度が高いUIコンポーネントだけ CSS Modules で書き直したり、CSS Modules をお試しで部分的に導入するといったことも可能です。

## `transition` や `transition-group` を使いたいときは CSS Modules は使えない?
以下のコードのように、カスタムトランジションクラス[^4]を利用すれば `<transition>`, `<transition-group>` のトランジションクラスを CSS Modules のものにできます：

```vue:TransitionExampleComponent.vue
<template>
  <transition
    :enter-active-class="$style.enter_active"
    :leave-active-class="$style.leave_active"
  >
    <p v-if="show">hello</p>
  </transition>
</template>

<style module>
.enter_active { /* ... */ }
.leave_active { /* ... */ }
</style>
```

`name` 属性の指定は必要なくなり、**より明示的にトランジションクラスを指定できます**。

注意として、 `:class` とは異なり `:enter-active-class="[$style.hoge, $style.fuga]"` のようにして複数クラスを指定することはできません。 `enter-active-class` などのカスタムトランジションクラスは文字列のみを受け取る想定だからです[^5]。
そのため、カスタムトランジションクラスは各々1つだけ設定することをおすすめします。

# 注意しなければならないこと
## 外側からスタイルの上書きをしない
UIコンポーネントの色や padding といったコンポーネントの**内側のスタイルに外側から干渉しない**ようにしましょう。
(margin はUIコンポーネントの外側のスタイルなので、問題ありません)

詳しくは [【Vue.js】Scoped CSSよりCSS Modulesの方がベターだった件 - CSS Modules にも気をつけるべきことがあるらしい](https://qiita.com/mascii/items/3202b9e18fd4a7366ac1#css-modules-%E3%81%AB%E3%82%82%E6%B0%97%E3%82%92%E3%81%A4%E3%81%91%E3%82%8B%E3%81%B9%E3%81%8D%E3%81%93%E3%81%A8%E3%81%8C%E3%81%82%E3%82%8B%E3%82%89%E3%81%97%E3%81%84) を参照してください。

## クラスセレクタ以外のセレクタの利用
### 要素型セレクタ
`p` や `li` といったタグに対する[要素型セレクタ](https://developer.mozilla.org/ja/docs/Web/CSS/Type_selectors)は、クラスセレクタとは異なり CSS Modules による名前が置換されるような仕組みは適用されません。
そのため、要素型セレクタを単独で利用すると**適用範囲がコンポーネントの外まで**広がってしまいます。

要素型セレクタを利用するとしても、クラスセレクタの子や子孫としての利用に絞った方が良いと思います:

```vue
<style module>
.list > li {
  color: #222222;
}
</style>
```

### 属性セレクタ
[属性セレクタ](https://developer.mozilla.org/ja/docs/Web/CSS/Attribute_selectors)(`[href="https://example.org"]` など)も要素型セレクタと同様に、CSS Modules によって名前が置換されるような仕組みは適用されません。

### IDセレクタ
`#app` のようなIDセレクタは、クラスセレクタと同様に名前が置換され `<div :id="$style.app">` のようにして利用可能ですが、使うメリットはないのでクラスセレクタで統一しましょう。

# 少し進んだ使い方
## カスタム注入名を使う
[vue-loader の CSS モジュールのページ](https://vue-loader-v14.vuejs.org/ja/features/css-modules.html)に書いてある通り、`<style>` の `module` 属性に値を与えることによって `$style` の名前を変更(`<style module="a">` なら `a`)したり、スタイルブロックを複数書くことができます。

以下のコードのように、コンポーネントを使う側から props でサイズや色を指定するような使い方が便利です:

```vue:App.vue
<template>
  <div>
    <CustomInjectNameExample size-class="large">
      Hello, World!
    </CustomInjectNameExample>
  </div>
</template>
```

```vue:CustomInjectNameExample.vue
<template>
  <div :class="[$style.container, $sizeStyle[sizeClass]]">
    <slot />
  </div>
</template>

<script>
export default {
  name: 'CustomInjectNameExample',
  props: {
    sizeClass: {
      type: String,
      required: true,
    },
  },
};
</script>

<style module>
.container { /* ... */ }
</style>

<style module="$sizeStyle">
.large { /* ... */ }
.small { /* ... */ }
</style>
```

### カスタム注入名を使うメリット
スタイルブロックを分けることによって、上のサイズの例と同様にして props で色を指定するためのスタイルブロック `<style module="$kindStyle">` を追加した場合に `size-class` で色の指定がされてしまうことを防げます:

```vue:App.vue
<template>
  <div>
    <CustomInjectNameExample size-class="large" kind-class="warning">
      OK
    </CustomInjectNameExample>

    <CustomInjectNameExample size-class="warning" kind-class="large">
      NG (スタイルが当たらないのでミスに気づくことができる)
      スタイルブロックを分けなかった場合はスタイルが当たってしまいます
    </CustomInjectNameExample>
  </div>
</template>
```

### スタイルブロックの壁は薄い
上記の `CustomInjectNameExample.vue` の `<style module>` に `.large { }` のクラスセレクタを定義し、 `$style` と `$sizeStyle` の内容を確認すると `large` が **同じ文字列に対応している**ことがわかります。
<img width="727" alt="same_value_of_class.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/db169468-bc45-f127-18a0-80209bbda8f8.png">

つまり、`$style.large` と `$sizeStyle.large` のどちらを `:class` で指定しても、両方のスタイルブロックの `.large { }` 内に書いたスタイルが当たってしまいます。スタイルブロック間で同じクラス名は用いない方が無難です[^6]。

:::note info
(2023/01/10 追記) [create-vue](https://github.com/vuejs/create-vue) で作成された Vite 4.0.4, Vue 3.2.45 の環境においては `$style.large` と `$sizeStyle.large` は異なる文字列となっていました。

![vite_css_modules.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/137045/b1bccb3f-3f9c-1e4b-8b31-e68ee4d84e7f.png)
:::

# おわりに
Vue.js の CSS 周りはハマりどころが多く、私の職場でも周知が足りなかったためにこの記事に書いた注意点をコードレビュー時に指摘させていただくことが何度かありました。
この記事や[2019/06 に書いた記事](https://qiita.com/mascii/items/3202b9e18fd4a7366ac1)が Vue.js を使っている開発現場で少しでもお役に立てれば幸いです。

[^1]: 詳しくは [【Vue.js】Scoped CSSよりCSS Modulesの方がベターだった件](https://qiita.com/mascii/items/3202b9e18fd4a7366ac1) を参照
[^2]: 詳しくは Vue.js 公式ドキュメントの [クラスとスタイルのバインディング](https://jp.vuejs.org/v2/guide/class-and-style.html) を参照 (`:class` は `v-bind:class` の省略記法であることに注意)
[^3]: `-` はマイナスの演算子と被ってしまうため。 camelCase でも問題ないです
[^4]: 詳しくは Vue.js 公式ドキュメントの [Enter/Leave とトランジション一覧](https://jp.vuejs.org/v2/guide/transitions.html) の「カスタムトランジションクラス」を参照
[^5]: Vue.js 公式ドキュメントの [クラスとスタイルのバインディング](https://jp.vuejs.org/v2/guide/class-and-style.html) に記載の通り、 `v-bind:class` と `v-bind:style` に限って特別な拡張機能を提供(配列記法など)されています
[^6]: webpack を利用していてどうしても `$style.large !== $sizeStyle.large` になるようにしたい場合、css-loader の設定の `getLocalIdent` 関数を変更する必要があります ([vuejs/vue-loader#1578](https://github.com/vuejs/vue-loader/issues/1578))