2019/06 に【Vue.js】Scoped CSSよりCSS Modulesの方がベターだった件 の記事を書いてから1年が経ちました。
この1年間でよくあった質問の回答や、少し進んだ使い方を紹介したいと思います。
よくある質問
$style. と書くのは面倒ではないか?
<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。
ポイントは3つです:
- 親コンポーネントと子コンポーネントの間で、クラス名の重複を気にする必要はない
-
:classで複数のクラスを当てるときは配列構文を用いる必要がある - snake_case でクラス名を定義するのがおすすめ3
テンプレートで class と :class は併記できる?
以下のコードのように Font Awesome などのクラスを class で、 <style module> で定義したクラスを :class で当てることができます:
<template>
<i class="far fa-lightbulb" :class="$style.light"></i>
</template>
CSS Modules で scss は使える?
Vue CLI 環境では sass と sass-loader が導入されていれば scss が使えます。
<style lang="scss" module> のようにスタイルブロックで lang="scss" を指定するようにします:
<style lang="scss" module>
.hoge {
> .fuga {
/* ... */
}
> .piyo {
/* ... */
}
}
</style>
Scoped CSS と CSS Modules は共存できる?
ひとつのプロジェクトの中で Scoped CSS と CSS Modules は共存できます。
(さらには、ひとつの .vue ファイルの中で <style scoped> と <style module> は共存できます)
実例として、Nuxt.js を用いて実装されている東京都 新型コロナウイルス感染症対策サイト(2023/05 公開終了)では、Scoped CSS が使われているコンポーネント(
GitHubで確認) と CSS Modules が使われているコンポーネント(
GitHubで確認)が混在していました。
利用頻度が高いUIコンポーネントだけ CSS Modules で書き直したり、CSS Modules をお試しで部分的に導入するといったことも可能です。
transition や transition-group を使いたいときは CSS Modules は使えない?
以下のコードのように、カスタムトランジションクラス4を利用すれば <transition>, <transition-group> のトランジションクラスを CSS Modules のものにできます:
<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 にも気をつけるべきことがあるらしい を参照してください。
クラスセレクタ以外のセレクタの利用
要素型セレクタ
p や li といったタグに対する要素型セレクタは、クラスセレクタとは異なり CSS Modules による名前が置換されるような仕組みは適用されません。
そのため、要素型セレクタを単独で利用すると適用範囲がコンポーネントの外まで広がってしまいます。
要素型セレクタを利用するとしても、クラスセレクタの子や子孫としての利用に絞った方が良いと思います:
<style module>
.list > li {
color: #222222;
}
</style>
属性セレクタ
属性セレクタ([href="https://example.org"] など)も要素型セレクタと同様に、CSS Modules によって名前が置換されるような仕組みは適用されません。
IDセレクタ
#app のようなIDセレクタは、クラスセレクタと同様に名前が置換され <div :id="$style.app"> のようにして利用可能ですが、使うメリットはないのでクラスセレクタで統一しましょう。
少し進んだ使い方
カスタム注入名を使う
vue-loader の CSS モジュールのページに書いてある通り、<style> の module 属性に値を与えることによって $style の名前を変更(<style module="a"> なら a)したり、スタイルブロックを複数書くことができます。
以下のコードのように、コンポーネントを使う側から props でサイズや色を指定するような使い方が便利です:
<template>
<div>
<CustomInjectNameExample size-class="large">
Hello, World!
</CustomInjectNameExample>
</div>
</template>
<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 で色の指定がされてしまうことを防げます:
<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 が 同じ文字列に対応していることがわかります。

つまり、$style.large と $sizeStyle.large のどちらを :class で指定しても、両方のスタイルブロックの .large { } 内に書いたスタイルが当たってしまいます。スタイルブロック間で同じクラス名は用いない方が無難です6。
(2023/01/10 追記) create-vue で作成された Vite 4.0.4, Vue 3.2.45 の環境においては $style.large と $sizeStyle.large は異なる文字列となっていました。
おわりに
Vue.js の CSS 周りはハマりどころが多く、私の職場でも周知が足りなかったためにこの記事に書いた注意点をコードレビュー時に指摘させていただくことが何度かありました。
この記事や2019/06 に書いた記事が Vue.js を使っている開発現場で少しでもお役に立てれば幸いです。
-
詳しくは Vue.js 公式ドキュメントの クラスとスタイルのバインディング を参照 (
:classはv-bind:classの省略記法であることに注意) ↩ -
-はマイナスの演算子と被ってしまうため。 camelCase でも問題ないです ↩ -
詳しくは Vue.js 公式ドキュメントの Enter/Leave とトランジション一覧 の「カスタムトランジションクラス」を参照 ↩
-
Vue.js 公式ドキュメントの クラスとスタイルのバインディング に記載の通り、
v-bind:classとv-bind:styleに限って特別な拡張機能を提供(配列記法など)されています ↩ -
webpack を利用していてどうしても
$style.large !== $sizeStyle.largeになるようにしたい場合、css-loader の設定のgetLocalIdent関数を変更する必要があります (vuejs/vue-loader#1578) ↩
