Vue.jsのScoped CSSは完全にはscopedじゃない、という話を聞いたので検証してみました。
結果、コンポーネント最上位の要素と、slotについては気をつける必要があることが分かりました。
参考記事
親コンポーネントのstyleが子コンポーネントに影響を与える仕組みは、以下が詳しいです。
- https://qiita.com/wintyo/items/dfc232255ad45fdf376f
- https://www.dkrk-blog.net/javascript/vue_scopedcss
検証環境
- OS: macOS Catalina 10.15.5
- ブラウザ: Google Chrome 83.0.4103.116(Official Build)(64bit)
- Vue.js: 2.6.11
## 検証1: 素朴にstyleを当てた場合 親、子、孫コンポーネントにそれぞれ違った色を割り当てました。 すると、各コンポーネントの最上位の要素には親コンポーネントのスタイルが適用されてしまいました。 ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/e5e31450-3b85-6cbc-80a6-0127f657f37a.png)
Parent.vue
<template>
<div class="root-div">
this is root div of parent
<h1 class="my-title">this is h1 of parent</h1>
<div class="h2-container">
<h2>this is h2 of parent</h2>
</div>
<Child />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import Child from "@/components/Child.vue";
export default Vue.extend({
name: "Parent",
components: {
Child,
},
});
</script>
<style scoped lang="scss">
.root-div {
color: red;
}
.my-title {
color: red;
}
h2 {
color: red;
}
</style>
Child.vue
<template>
<div class="root-div">
this is root div of child
<h1 class="my-title">this is h1 of child</h1>
<div class="h2-container">
<h2>this is h2 of child</h2>
</div>
<GrandChild />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import GrandChild from "@/components/GrandChild.vue";
export default Vue.extend({
name: "Child",
components: {
GrandChild,
},
});
</script>
<style scoped lang="scss">
.root-div {
color: blue; // こいつが効いていない
}
.my-title {
color: blue;
}
h2 {
color: blue;
}
</style>
GrandChild.vue
<template>
<div class="root-div">
this is root div of grandchild
<h1 class="my-title">this is h1 of grandchild</h1>
<div class="h2-container">
<h2>this is h2 of grandchild</h2>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
name: "GrandChild",
});
</script>
<style scoped lang="scss">
.root-div {
color: green;
}
.my-title {
color: green;
}
h2 {
color: green;
}
</style>
## 検証2: 全称セレクタ(*)を使った場合 `*`を使って乱暴にstyleを指定した場合どうなるかを見てみました。 結果、検証1と同じく、各コンポーネント最上位の要素だけは親コンポーネントのstyleの影響が出ました。 ※検証1のコードから、Child.vueだけ変更しました。 ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/f42d3974-1134-884b-00ce-263a9c804efd.png)
Child.vue
<template>
<div class="root-div">
this is root div of child
<h1 class="my-title">this is h1 of child</h1>
<div class="h2-container">
<h2>this is h2 of child</h2>
</div>
<GrandChild />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import GrandChild from "@/components/GrandChild.vue";
export default Vue.extend({
name: "Child",
components: {
GrandChild,
},
});
</script>
<style scoped lang="scss">
.root-div * {
color: blue;
}
* {
color: blue;
}
* * {
color: blue;
}
</style>
## 検証3: globalのcssがある場合 当たり前ですが、ページ全体に適用されているcssがある場合、競合します。 例えば、ページ全体に`* { color: black!important }`を当てたら、すべて黒になります。 ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/647bdd46-1679-a8ee-86a8-7546a23e5cc7.png)
App.vue
<style lang="scss">
* {
color: black!important; // プロダクトコードで書いたら殴られるやつ
}
</style>
もちろん、`!important`を付けなければ、コンポーネント内のcssが優先的に適用されます。 (この辺はScoped CSSというよりはCSSそのものの話) ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/da7e1b8f-91b6-00f0-fa1b-7d72e92d4944.png)
App.vue
<style lang="scss">
* {
color: black;
}
</style>
## 検証4: slotを使った場合 slotを使って、親から子へタグを渡した場合、渡したタグには親コンポーネントのstyleが適用されました。 ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/fd18c235-5cbc-5c67-564d-86a7d3ba982f.png)
Parent.vue
<template>
<div class="root-div">
this is root div of parent
<h1 class="my-title">this is h1 of parent</h1>
<div class="h2-container">
<h2>this is h2 of parent</h2>
</div>
<Child>
<!-- 親からタグごとslotを挟むと、親のstyleが適用される -->
<template #my-slot>
<p>slot: this is p of parent</p>
<div>
<h2>slot: this is h2 of parent</h2>
</div>
</template>
</Child>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import Child from "@/components/Child.vue";
export default Vue.extend({
name: "Parent",
components: {
Child,
},
});
</script>
<style scoped lang="scss">
.root-div {
color: red;
}
.my-title {
color: red;
}
h2 {
color: red;
}
p {
color: red;
}
</style>
Child.vue
<template>
<div class="root-div">
this is root div of child
<h1 class="my-title">this is h1 of child</h1>
<div class="h2-container">
<h2>this is h2 of child</h2>
</div>
<slot name="my-slot">
<p>slot: this is p of child</p>
<div>
<h2>slot: this is h2 of child</h2>
</div>
</slot>
<GrandChild />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import GrandChild from "@/components/GrandChild.vue";
export default Vue.extend({
name: "Child",
components: {
GrandChild,
},
});
</script>
<style scoped lang="scss">
.root-div {
color: blue;
}
.my-title {
color: blue;
}
h2 {
color: blue;
}
p {
color: blue;
}
</style>
一方、親から子へ文字列だけを渡した場合、子のstyleが適用されました。 ![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/162371/f7fedb5e-9783-7cf4-1d6d-e9ddc7897ede.png)
Parent.vue
<template>
<div class="root-div">
this is root div of parent
<h1 class="my-title">this is h1 of parent</h1>
<div class="h2-container">
<h2>this is h2 of parent</h2>
</div>
<Child>
<!-- 親から文字列だけを渡すようにすれば、親のstyleは適用されない -->
<template #my-slot-p>slot: this is p of parent</template>
<template #my-slot-h2>slot: this is h2 of parent</template>
</Child>
</div>
</template>
<script lang="ts">
import Vue from "vue";
import Child from "@/components/Child.vue";
export default Vue.extend({
name: "Parent",
components: {
Child,
},
});
</script>
<style scoped lang="scss">
.root-div {
color: red;
}
.my-title {
color: red;
}
h2 {
color: red;
}
p {
color: red;
}
</style>
Child.vue
<template>
<div class="root-div">
this is root div of child
<h1 class="my-title">this is h1 of child</h1>
<div class="h2-container">
<h2>this is h2 of child</h2>
</div>
<p><slot name="my-slot-p">slot: this is p of child</slot></p>
<div>
<h2><slot name="my-slot-h2">slot: this is h2 of child</slot></h2>
</div>
<GrandChild />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import GrandChild from "@/components/GrandChild.vue";
export default Vue.extend({
name: "Child",
components: {
GrandChild,
},
});
</script>
<style scoped lang="scss">
.root-div {
color: blue;
}
.my-title {
color: blue;
}
h2 {
color: blue;
}
p {
color: blue;
}
</style>
## で、結局何に気をつければいいのか * コンポーネント最上位の要素になるべくstyleを当てない * 当てざるを得ない場合、`.component-name-root { display: flex }`のように、重複しないクラス名を付けて、styleを指定する * 自前でslotを作る場合、極力DOM構造は子コンポーネント側で定義する * デザインフレームワーク(vuetifyとか)の制約上slotにDOMを渡さざるを得ない場合は、予期せぬstyleが適用されないよう気をつける
所感
完全でないとはいえ、知らずに使っていてもそんなに気にならない程度にはscopedにできていると感じました。
個人的にはslotを使わないため、各コンポーネントの最上位のクラス名だけ気をつければ問題なし! という印象です。
Scoped CSS便利。