Vuetify は、とても使いやすいVue UIコンポーネントを提供してくれるライブラリなのですが、リセットCSSが強制的にページ全体に適用されてしまうため、既存ページの一部分にVuetifyを使いたくてもその外側のデザインが崩れてしまうことがあります。

ここではその問題を回避する方法について紹介します。

リセットCSSとは

リセットCSSとは各ブラウザがデフォルトで適用するCSSの差異の影響で画面のデザインが変わってしまわない様に、ブラウザが適用するCSSを打ち消すためのものらしいです。

VuetifyのリセットCSS

VuetifyのリセットCSSは以下で、これが強制的に適用されます。
https://github.com/vuetifyjs/vuetify/blob/master/packages/vuetify/src/styles/generic/_reset.scss

例えば、以下の様な結構強烈な設定があります。

button,
input,
select,
textarea {
  background-color: transparent;
  border-style: none;
}

こんなのがあるとVuetifyの適用範囲の外側はもちろん崩れまくりです。
もちろん困っている人も多く github issue も上がっていますが、未だ解決されていません。

対応方法

というわけで、既存ページの一部にVuetifyを適用する場合は、対策を取る必要があります。
具体的には、webpackのLoaderの
sass-loader->css-loader->style-loader
という一連の処理に
sass-loader->postcss-loader->css-loader->style-loader
の様に、postcss全てのCSSセレクタの頭にプロジェクト固有の#idをつけるという処理を追加しました。
例えば以下の様なイメージです。

/* 変更前 */
button,
input, {
  border-style: none;
}

/* 変更後 */
#myapp button,
#myapp input {
  border-style: none;
}

これにより、#myappの外側の要素には一切影響を与えないことが担保されます。

実装

ライブラリインストール

npm install postcss --save-dev
npm install postcss-loader --save-dev
npm install postcss-prefix-selector --save-dev
npm insatll autoprefixer --save-dev

App.vue

Vuetifyの適用範囲は<v-app>で囲む必要がありますが、その親要素にidを設定します。

<template>
  <div id="myapp">
    <v-app>
      <!-- 省略 -->
    </v-app>
  </div>
</template>

webpack.config.js

ポイントを書いておきます。

  • loaderは下から順に適用されるので、以下の順番に処理が行われる
    1. sass-loaderで、sassをcssに変換する
    2. postcss-loaderで、cssをさらに別のcssに変換する
    3. css-loaderで、cssファイルを文字列に変換してJSで扱える様にする
    4. style-loaderで、CSS文字列を<style>としてDOMに挿入する
  • postcss-loader内でpostcss-prefix-selectorを使って、cssセレクタの頭に#myappを追加している
    const prefixer = require('postcss-prefix-selector');
    // 省略
      module: {
        rules: [
          {
            test: /\.(css|scss|sass)$/,
            use: [
              'vue-style-loader',
              'css-loader',
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    plugins: [
                      prefixer({
                        prefix: '#myapp',
                        transform: (prefix, selector, prefixedSelector) => {
                          if (selector.startsWith('html') || selector.startsWith('body')) {
                            return prefix + selector.substring(4);
                          }
                          return prefixedSelector;
                        }
                      })
                    ]
                  }
                }
              },
              {
                loader: 'sass-loader',
                options: {
                  // 省略
                },
              },
            ],
          },
        ]
      }

さいごに

正直、Vuetify本体で対応して欲しいところですが、なかなか対応してくれません。どうやらCDNを利用するケースも合わせて一律の対応するのが難しいっぽいです。

とはいえ、本対応を行うことで、自分が使っている範囲では特に問題はなくなりましたので、まあ良しとしたいと思います。