scoped CSSと子コンポーネントの関係
JavaScript, Vuevue-loader では、style
要素に scoped
を指定して、コンポーネントに CSS のスコープを閉じられる。
ただ、レイアウトの調整目的で、親コンポーネントから子コンポーネントにスタイルを当てたいと思う時がある。
その時の scoped CSS の挙動を確認するため、少し検証した。
基本形
下記コードでは、親コンポーネントが、子コンポーネントに border
と margin
スタイルを設定している。
child.vue
<template>
<section>
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
</template>
parent.vue
<template>
<div>
<h1>子コンポーネント集</h1>
<child-component class="child" />
<child-component class="child" />
<child-component class="child" />
</div>
</template>
<script>
import ChildComponent from "./components/child.vue";
export default {
components: {
ChildComponent
}
};
</script>
<style scoped>
.child {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
</style>
生成されるHTMLは以下。
html
<div data-v-6a9a0e7a>
<h1 data-v-6a9a0e7a>子コンポーネント集</h1>
<section data-v-6a9a0e7a class="child">...</section>
<section data-v-6a9a0e7a class="child">...</section>
<section data-v-6a9a0e7a class="child">...</section>
</div>
また、下記のstyleが追加される。
css
.child[data-v-6a9a0e7a] {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
vue-loader は scoped なファイルに対して、 hash-sum でハッシュ値を生成し、data-v-
を合わせた data 属性を要素に追加する。
それを CSS セレクタとした style 要素を追加することで、擬似的なスコープを実現している。
また、子のルートとなる要素に data
属性や class
がマージされる。
上のコードでは、子のルートとなる section
要素に、data-v-6a9a0e7a
属性と .child
が追加される。
そのため、親で指定したmarginやborderなどのプロパティが、子に適用される。
子がFunctional Componentの場合
Functional Component の場合は、親の class がマージされない。
前セクションの例だと、親で指定した .child が子のルートから消えるので、スタイルが適用されなくなる。
ただし、親から Functional Component に渡されるデータは、context
を通して取得できる。
その中に、属性や listener を受け取る data がある。data.staticClass
を子のルートにバインドすることで、親側で付けた class を追加できる。
child.vueのテンプレート
<template functional>
<section :class="data.staticClass">
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
</template>
描画関数とJSX — Vue.js
Functional Componentとscoped css - ぬまのそこ
子のルートにclassがある場合
子コンポーネントを、一旦普通のコンポーネントに戻し、ルートとなる section
要素に child
を追加する。
child.vueのtemplate
<template>
<section class="child">
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
</template>
子供が増えてしまった。
html
<div data-v-6a9a0e7a>
<h1 data-v-6a9a0e7a>子コンポーネント集</h1>
<section data-v-6a9a0e7a class="child child"></section>
<section data-v-6a9a0e7a class="child child"></section>
<section data-v-6a9a0e7a class="child child"></section>
</div>
ただ、子コンポーネントに適用されている data 属性は同じなので、スタイルは適用される。
css
/* .styleにはdata-v-6a9a0e7aが含まれているので適用 */
.child[data-v-6a9a0e7a] {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
つまり子に .foo
を持たせて、親側にも .foo
セレクタを追加すれば、親で指定したスタイルが適用される。
child.vueのtemplate
<template>
<section class="child foo">
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
</template>
parent.vueのstyle
<style scoped>
.child {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
/* foo */
.foo {
display: none;
}
</style>
html
<div data-v-6a9a0e7a>
<h1 data-v-6a9a0e7a>子コンポーネント集</h1>
<section data-v-6a9a0e7a class="child foo child">...</section>
<section data-v-6a9a0e7a class="child foo child">...</section>
<section data-v-6a9a0e7a class="child foo child">...</section>
</div>
css
.child[data-v-6a9a0e7a] {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
/* .fooの要素もdata-v-6a9a0e7aなので、スタイルが適用される */
.foo[data-v-6a9a0e7a] {
display: none;
}
親から子の中の要素にスタイルを適用する場合
子コンポーネントには h2
要素があるけど、主力される HTML には data-v-6a9a0e7a
が付いてない。
html
<section data-v-6a9a0e7a class="child">
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
これにより、親コンポーネントからスタイルを適用することはできない。
parent.vueのstyle
<style scoped>
/* 親からの攻撃 */
.child h2 {
font-size: 100px;
}
</style>
なぜかと言えば、セレクタが .child h2[data-v-6a9a0e7a]
になるため。
css
.child h2[data-v-6a9a0e7a] {
font-size: 100px;
}
h2 要素には data 属性が付けられていないので、このスタイルは適用されない。
逆にセレクタが .child[data-v-6a9a0e7a] h2
であれば、スタイルが適用される。
それを実現するのが >>>
セレクタです。
(SCSS など一部のプリプロセッサを用いる場合、かわりに /deep/
を使用する)
parent.vueのstyle
<style scoped>
/* >>> を追加 */
.child >>> h2 {
font-size: 100px;
}
</style>
こうするとスタイルが適用され、親から子の要素に干渉できる。
css
.child[data-v-6a9a0e7a] h2 {
font-size: 100px;
}
子が Functional Component で、ルートとなる要素に .child
がある場合、その子要素にも data 属性がマージされる。
つまり、先ほどの >>>
同様、style が適用されてしまう。
child.vue
<template functional>
<section class="child">
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
</template>
html
<div data-v-6a9a0e7a>
<!-- 子要素にもdata-v-6a9a0e7aが付く -->
<section data-v-6a9a0e7a class="child">
<h2 data-v-6a9a0e7a>子コンポーネントです</h2>
<p data-v-6a9a0e7a>子コンポーネントの文章です</p>
</section>
</div>
子にもscoped CSSがある場合
子にも scoped が設定されている場合、どうなるのか。
child.vue
<template>
<section>
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
</template>
<style scoped>
/* scopedなh2のstyle */
h2 {
font-size: 20px;
}
</style>
parent.vue
<template>
<div>
<h1>子コンポーネント集</h1>
<child-component class="child"/>
<child-component class="child"/>
<child-component class="child"/>
</div>
</template>
<script>
import ChildComponent from "./components/child.vue";
export default {
components: {
ChildComponent
},
};
</script>
<style scoped>
.child {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
</style>
上のコードでは、親と子の両方に、それぞれ別の scoped CSS が付与されている。
見た目は問題ない。子コンポーネント見出しの font-size を変えたので、そちらが適用される。
一方で HTML や CSS を見ると、子コンポーネントのルートや子要素に、新たな data 属性が増えている。
html
<!-- data-v-efbcda1eが増えている -->
<section data-v-efbcda1e data-v-6a9a0e7a class="child">
<h2 data-v-efbcda1e>子コンポーネントです</h2>
<p data-v-efbcda1e>子コンポーネントの文章です</p>
</section>
css
/* 親コンポーネント */
.child[data-v-6a9a0e7a] {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
/* 子コンポーネント(data属性が親と違う) */
h2[data-v-efbcda1e] {
font-size: 20px;
}
子コンポーネントのルートには親の data 属性が追加されていながら、孫となる要素には追加されていない。
これは、scoped でない style から身を守る仕組みの 1 つにもなっている。
css
/*
* scopedでないCSSの猛攻
* 優先度が低くなるので効かない
*/
h2 {
font-size: 100px;
}
section h2 {
font-size: 100px;
}
/* 優先度が高く適用される */
h2[data-v-efbcda1e] {
font-size: 14px;
}
ただし、先述した >>>
や /deep/
セレクターの影響を受ける。
css
// 強い
.child[data-v-6a9a0e7a] h2 {
font-size: 100px;
}
// 弱い
h2[data-v-efbcda1e] {
font-size: 12px;
}
scoped を指定した単一ファイルコンポーネントには、その子要素(と子コンポーネントのルート)に data 属性が付与される。
子がFunctional Componentで、scoped CSSが適用されている場合
Functional Component に scoped を付けた場合はどうなるのか。
child.vue
<template functional>
<section :class="data.staticClass">
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
</template>
<style scoped>
/* scopedを作成 */
</style>
この場合は、親要素の data 属性がマージされず、独自の data 属性だけが追加される。
そのため、親コンポーネントからの style が適用されなくなる。
html
<div data-v-6a9a0e7a>
<!-- 親コンポーネントのdata-v-6a9a0e7aがつかない -->
<section data-v-efbcda1e class="child">
<h2 data-v-efbcda1e>子コンポーネントです</h2>
<p data-v-efbcda1e>子コンポーネントの文章です</p>
</section>
</div>
css
/* 子コンポーネントにclassはついているが、data属性がマージされていないので適用されない */
.child[data-v-6a9a0e7a] {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
margin: 0;
}
対処としては親の data 属性をセレクタに乗せるか、親から data 属性をマージするか、などが挙げられる。
イレギュラーな形ながら、>>>
をルートにして。child を指定すると、
親にあたる data-v-6a9a0e7a の .child
にスタイルを適用できる。
parent.vueのstyle
<style scoped>
>>> .child {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
</style>
css
/* data-v-6a9a0e7a(親)の配下にある.childなので、これは適用される */
[data-v-6a9a0e7a] .child {
border-bottom: 1px solid #ccc;
padding-bottom: 30px;
margin-bottom: 30px;
}
また、更にイレギュラーだけど、親の _scopedId
を参照して属性を追加できる。(lint に引っかかる)
child.vue
<template functional>
<section :[parent.$options._scopeId]="true" :class="data.staticClass">
<h2>子コンポーネントです</h2>
<p>子コンポーネントの文章です</p>
</section>
</template>
<style scoped></style>
$el
から dataset を取得し props として渡す方法などもあるけど、現実的でない。
template
層で調整するのが無難。
親がslotからstyleを当てる場合
slot では、本来は孫となる要素にもスタイルを適用できる。
child.vue
<template>
<section>
<h2>子コンポーネントです</h2>
<slot name="paragraph"></slot>
</section>
</template>
<style scoped></style>
parent.vue
<template>
<div>
<h1>子コンポーネント集</h1>
<child-component class="child">
<template #paragraph>
<p>最初の子コンポーネントですよ</p>
</template>
</child-component>
</div>
</template>
<style scoped>
p {
font-size: 100px;
}
</style>
上のコードでは、親で paragraph
スロットの中に p
要素があって、さらにスタイルを設定している。
この場合のスコープは親側になり、孫となる要素にも data-v-7ba5bd90
がマージされ、スタイルが適用される。
html
<div data-v-7ba5bd90>
<section data-v-efbcda1e data-v-7ba5bd90 class="child">
<h2 data-v-efbcda1e>子コンポーネントです</h2>
<!-- 親のdata属性がついている -->
<p data-v-7ba5bd90 data-v-efbcda1e>最初の子コンポーネントですよ</p>
</section>
</div>
親にスタイルがない場合は、そのまま子のスタイルが適用される。
child.vue
<template>
<section>
<h2>子コンポーネントです</h2>
<slot name="paragraph"></slot>
</section>
</template>
<style scoped>
p {
font-size: 12px;
}
</style>
css
p[data-v-efbcda1e] {
font-size: 12px;
}
p
要素を子に置いて slot を囲む場合、data
属性がマージされないので、親のスタイルは適用されない。
child.vue
<template>
<section>
<h2>子コンポーネントです</h2>
<p>
<slot name="paragraph"></slot>
</p>
</section>
</template>
html
<section data-v-efbcda1e data-v-7ba5bd90 class="child">
<h2 data-v-efbcda1e>子コンポーネントです</h2>
<p data-v-efbcda1e>最初の子コンポーネントですよ</p>
</section>
ちょっとわかりづらいのでまとめた。
- 親の slot 内に要素があり、style を指定している場合、親の style が適用される
- 親の slot 内に要素があるが style がない場合、子の style が適用される
- 子の中で slot を囲んだ要素には、子の style が適用される
ちなみに親子で同じセレクタがあると、いったん style を生成しつつ、子の style を親が打ち消す。
html
<style type="text/css">
/* 子 */
p[data-v-71a7d33c] {
font-size: 12px;
}
</style>
<style type="text/css">
/* 親が打ち消す */
p[data-v-7ba5bd90] {
font-size: 100px;
}
</style>
HMR で scoped CSS を追加した時に、この順番が入れ替わってしまうことがあった(執筆時点)。
html
<style type="text/css">
/* 本来下に来るもの */
p[data-v-7ba5bd90] {
font-size: 100px;
}
</style>
<style type="text/css">
/* 子に追加したstyleが当たってしまっている */
p[data-v-71a7d33c] {
font-size: 12px;
}
</style>
まとめ
- scoped CSS は、ハッシュ値を要素と style に紐付けて、擬似的なスコープを生成する
- class、ハッシュ値などの属性は子要素と子コンポーネントのルートに渡され、孫には渡されない
>>>
でセレクタの順番を変えて、子コンポーネントの子要素に style を適用できる- Functional Component は、基本的に class や data 属性がマージされない
- slot は、親で指定した style をもとに、孫となる要素に data 属性をマージする