Scoped CSSの問題点
Vue.jsのscoped cssは気楽にコンポーネント単位のスタイル当てができますが、完璧ではありません。
場合によってスタイルのバッティングが起きてしまうことがあるのですが、思っていたよりその現象が発生したのでどんな時に起こるのかをまとめました。
バッティングするサンプルコードはこちらに置きましたので、興味がある方はご覧になってください。
https://codesandbox.io/s/63q0l71rr
Vue.jsのscoped CSSの原理とバッティングの理由
詳細はドキュメントに譲りますが、簡単にいうと、コンポーネントごとにdata-v-[hash]
が振られて、その属性とセットでスタイルが当たるものとなっています。これによってこの属性はコンポーネントごとに違うものが当たるため、コンポーネント単位でのスタイルが当たるということです。
ここで問題になるのが、設定される属性の数です。例えばコンポーネントのルート要素では、親コンポーネントの属性と子コンポーネントの属性の2つが当たってしまい、それぞれのコンポーネントで設定したクラスが当たってしまう場合が起きてしまいます。
バッティングする状況例
子コンポーネントのルートクラスとバッティングする
一番多いのは子コンポーネントのルートで設定したクラスとのバッティングかなと思います。親から呼び出すと、親コンポーネントと子コンポーネントの属性が付与されるので、ここでバッティングします。
下の例でいうと.componentがバッティングします。
僕は割とサイズとかの指定をするクラスでラップしてからコンポーネントを呼ぶことが多いのですが、その時の名前が子コンポーネントのルートクラスと同じ名前だとうっかりバッティングしてしまいます。
こちらの記事でもこの話が挙げられています。
vue-loaderのScoped CSSのスタイルが子コンポーネントのルート要素に効いてしまって辛い
<template lang="pug">
//- ルートのクラスが親が定義したクラスとバッティングする
.component
.title タイトル
</template>
<style lang="scss" scoped>
.component {
background-color: #f0f0f0;
}
</style>
<template lang="pug">
div
p スタイルのバッティングをするコンポーネント
.component
CollisionComponent
</template>
<script>
import CollisionComponent from "./components/CollisionComponent";
export default {
name: "App",
components: {
CollisionComponent
}
};
</script>
<style lang="scss" scoped>
.component {
border: solid 1px black;
padding: 10px;
}
</style>
対策
一番単純な対策はdivでラップしちゃうことですね。ただこれをするとdiv要素が増えちゃうので困りものではあります。
<template lang="pug">
//- ルートのクラスが親が定義したクラスとバッティングするのでdivでラップする
div
.component
.title タイトル
</template>
<style lang="scss" scoped>
.component {
background-color: #f0f0f0;
}
</style>
slotによって配信されるクラスが子コンポーネントのスタイルとバッティングする
結構辛い思いをするのはこのslotによるバッティングだと思います。slotで配信するもの全てが親コンポーネントと子コンポーネントの属性の2つが付与されます。個人的には親コンポーネントだけでいいと思うんですけどね・・・。
例では.titleがバッティングします。
<template lang="pug">
//- ルートのクラスが親が定義したクラスとバッティングする
.component
.title タイトル
.content
//- slotで定義されるクラス名がここで使われる名前と同じだとバッティングする
slot
</template>
<style lang="scss" scoped>
.component {
background-color: #f0f0f0;
}
.title {
color: red;
}
</style>
<template lang="pug">
div
p スタイルのバッティングをするコンポーネント
.component
CollisionComponent
//- ここで設定したスタイルだけかと思ったら実は子コンポーネントで設定したスタイルも当たってしまう
.title SLOTコンテンツ
</template>
<script>
import CollisionComponent from "./components/CollisionComponent";
export default {
name: "App",
components: {
CollisionComponent
}
};
</script>
<style lang="scss" scoped>
.component {
border: solid 1px black;
padding: 10px;
}
.title {
font-weight: bold;
}
</style>
余談ですがslotの場所にコンポーネントを入れた場合は、親と子に加えて、slotに入るコンポーネントの属性の3つが付与されてました・・・。
対策
これは結構難しいのですが、親子セレクタを使って深い階層にある同じクラス名はスタイルが当たらないようにするという方法があります。ただしルートのクラスはどうしてもバッティング対象になるため、そこは名前だけでスタイル当てないようにするか、絶対にぶつけないようなクラス名にするとかの工夫が必要になります・・・。
<template lang="pug">
//- slotを使う場合はなんとかバッティングしないようなクラス名をルートにつける(_ + ファイル名とか?)
._safe-component
.title タイトル
.content
//- slotで定義されるクラス名がここで使われる名前と同じになる場合がある
slot
</template>
<style lang="scss" scoped>
// ルートはどうしようもないのでバッティングしないクラス名にする
._safe-component {
background-color: #f0f0f0;
// 親子セレクタにして孫に.titleがあってもスタイルを当てないようにする
> .title {
color: red;
}
}
</style>
まとめ
VueのScoped CSSはとても気軽にコンポーネント単位のスタイル当てができて便利なのですが、実際使っていくと割とバッティングすることがあるので注意しましょう。特にslotによる配信は何が入るか全然わからない上、slotの中身全てが親と子の属性がつけられるのでバッティングする可能性が非常に高いです。
より厳密にモジュール化したいならCSS modulesにすればいいという話になりますが、scoped CSSでも属性さえ余計に付与しなければ解決するような話なので、なかなか惜しいなぁと思いました。