この記事は「Vue.jsを使ったコンポーネント指向開発におけるCSS」ということで、社内勉強会でLTした内容をまとめたものになります。
具体的には、下記2点について記述しました。
- vue.jsのscoped CSSの仕組みと注意点
- 共通スタイルの概要と設計方針について
scoped CSSの概要
Vue.jsでは、scopde CSSを使ってcssの適応範囲をコンポーネント内に限定することが可能です。
例えば、下記のように.text-redクラスをscopde cssでh2タグに付与
すると...
<template>
<h2 class="default-text-color">{{ title }}</h2>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const title = ref("子コンポーネントです!");
return {
title
};
}
});
</script>
// style scopedとすることで、コンポーネントのCSSにスコープをもたせている。
<style scoped>
.default-text-color {
color: red;
}
</style>
生成されるDOMは下記になります。
<h2 class="default-text-color" data-v-2dcc19c8>子コンポーネントです!</h2>
<style>
.default-text-color[data-v-2dcc19c8] {
color: red;
}
</style>
この結果から、scoped cssでは「data-v-hogehogeというカスタムデータ属性」が付与されていることがわかります。
そして、このカスタムデータ属性はコンポーネント単位で異なります。それによって、外部コンポーネントに影響を与えずにスタイルを適応する仕組みを実現しています。
一方で、スコープを持たない親コンポーネントのcssや共通スタイルの影響は受けるので注意が必要です。
例えば、さきほどの例において、外部コンポーネントで.default-text-color {color: blue !important;}
などと指定されていた場合、テキストのカラーが青色に上書きされてしまいます。
scoped cssを使ったコンポーネント間でも注意が必要なケース
コンポーネント間のスタイル競合については、Scoped CSSを使うことで概ね防ぐ事が可能ですが、注意が必要なケースがあります。
それは、子コンポーネントのルート要素だけは親コンポーネントのスコープも持つ点です。
実際のソースコードを用いて、どういうことか見ていきます。
<template>
<div class="container">
<div class="title-wrapper">
<h2>{{ parentTitle }}</h2>
<Child />
</div>
</div>
</template>
<script lang="ts">
import Child from "./components/Child.vue";
import { defineComponent, ref } from "vue";
export default defineComponent({
components: {
Child
},
setup() {
const parentTitle = ref("回転してほしい。");
return {
parentTitle
};
}
});
</script>
<style scoped>
.container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 100vh;
}
.title-wrapper {
color: blue;
animation: rotateAnimation 5s ease-in-out infinite;
}
@keyframes rotateAnimation {
50%{ transform: rotate(360deg) }
}
</style>
<template>
<h2 class="title-wrapper">{{ childTitle }}</h2>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const childTitle = ref("回転してほしくない。");
return {
childTitle
};
}
});
</script>
<style scoped>
.title-wrapper {
// ルート要素のクラス名が親コンポーネント側とかぶっておち上書きされるため、important指定。(importantは極力控えるべきなので、本来はクラス名を変えてバッティングしないようにすべき。)
color: red !important;
}
</style>
上述のコードでは、親・子コンポーネントそれぞれスコープを設定しています。
しかし、下記gifのように、親コンポーネント側だけで指定しているアニメーションのスタイルが子コンポーネントのルート要素にも適応されてしまっていました。
これは、子のルート要素に親のスコープのデータ属性も付与されてしまっているためでした。
<div class="title-wrapper" data-v-7ba5bd90>
<h2 data-v-7ba5bd90>回転してほしい。</h2>
// 子コンポーネントのルート要素
<h2 class="title-wrapper" data-v-2dcc19c8 data-v-7ba5bd90>回転してほしくない。</h2>
</div>
このように、子コンポーネントは親のデータ属性も持つことからスコープ化している場合でも注意が必要です。(とりあえず重複するクラス名をつけないよう徹底するのが無難そう。)
- scoped CSSの特徴まとめ
- cssの適応範囲をコンポーネント内に閉じ込める
- 自身はグローバルcssの影響を受ける
- 子コンポーネントのルート要素は、呼び出し元のスコープも持つ(クラス名が重複していると、親コンポーネントのスタイルも適応されてしまう)
scoped CSSはコンポーネント単位でCSSをカプセル化できるので、BEMなど厳格なスタイルルールを作らなくても、壊れにくい設計ができるのは魅力的ですね。
vueにおける共通スタイル(リセットCSS、ユーティリティクラス)について
リセットCSS
リセットCSSとは、ブラウザごとにデフォルトで付与されているスタイルの差異をなくすためのCSS設定のこと。
デフォルトスタイルの初期化具合に応じて、ノーマライズCSSやサニタイズCSSといったものに別れていたりします。詳しくは、下の記事を参照ください。
また最近はcssフレームワーク側で、よしなにやってくれている印象もあります。
例えば、tailwind CSSだとPreflightというnormalize.cssをカスタマイズしたパッケージ
が用意されています。
tailwindcss公式ドキュメント preflightについて
リセットCSSに関しては、vueを使ったコンポーネント開発においても、sassを使って共通スタイルとして切り出す or フレームワーク側にまかせる形でとくに問題はない印象です。
ユーティリティクラスについて
ユーティリティクラスとは、クラス名とプロパティが1対1の関係になっているものです。
例えば、下記のようなものです。
// テキスト色のユーティリティクラス
.text-red {
color: red;
}
// 余白調整用のユーティリティクラス
.pb-3 {
padding-bottom: 16px;
}
スコープ化された各コンポーネント内で毎回定義するのはしんどいような、汎用的なクラスを共通ファイルに切り出して運用するのが良いかと思います。
あるいは、ユーティリティファーストなcssフレームワークを導入するのも手だと思います。(今だとtailwindがファーストチョイスな感じ。)
ユーティリティ指向によせるかどうかは、プロジェクトやチーム状況によっても良し悪しがかわってくる印象があるので、そのあたりの詳細について知見がたまったら、また噛み砕いて記事にしたいなと思っています。
今回はここまでになります。
その他、参考にさせていただいた資料
- みんなのVue.js 現場で役立つ実践ノウハウ
- Scoped CSSにおけるCSS設計手法