Vue – Mixing SASS with SCSS, with Vuetify as an Example

  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    Jul. 30, 2019
    Last Updated:
    Jul. 30, 2019
  • classTags
    classClick for Tags

The Vue CLI is an impressive tool that, similar to create-react-app, boostraps and automates a bunch of the Vue setup process. Unfortunately, the “magic” that makes the CLI and Vue so easy to use, also abstracts away a lot of what it is doing under the hood and makes it a little difficult to understand how to deal with unexpected issues, such as a SASS vs SCSS conflict when trying to use SCSS with Vuetify:

Error: Semicolons aren’t allowed in the indented syntax.

I got this error when trying to load a scss file through vue.config.js, which is the method that Vue officially recommends. If you search around, you can find that this is a known issue with Vue and how it uses webpack. If you selected SASS as one of the options while setting up the Vue CLI, under the hood Vue has installed and configured both “vue-loader” and  “sass-loader” to handle the Sass->CSS conversion. The specific issue is that “vue-loader”, using “sass-loader”, can handle both SASS (indented syntax) and SCSS (CSS3 superset) formatted Sass files, but will run into a conflict if trying to mix them at a global level with loaderOptions.sass.data. And in my situation, I’m trying to combine Vuetify, which uses the SASS syntax, with my own style file, which uses SCSS syntax.

If you want to jump right to the solution click here, otherwise, keep reading as I dig into the specifics of this issue.

What this issues is not about:

First, let’s try to make it clear what this issue is not. This issue is not simply about getting SASS/SCSS to work with Vue. That is pretty simple, assuming you have “sass-loader” installed. There are a few options, but the easiest is to simply stick your Sass into a style tag, like so:

<template>
    <div class="login">
        <button>Click to Login</button>
    </div>
</template>

<script>
</script>

<style lang="scss">
.login {
    button {
        color: green;
    }
}
</style>

Or, you could “import” the Sass style file in your main JS file that loads Vue. For example:

import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import './registerServiceWorker';
import vuetify from './plugins/vuetify';

import './styles/App.scss';

Vue.config.productionTip = false;

new Vue({
    router,
    store,
    vuetify,
    render: (h) => h(App),
}).$mount('#app');

The real issue: Global vs Non-Global

The real issue here is how to inject global styles across your entire Vue app, using Sass. The common reason why this is desired is Sass variables. For example, a common dev practice is to create a Sass variable file, maybe “_variables.scss“, which has variables such as “$darkColor: #222222;“, and then in a Vue component file (SFC), reference that variable value in the style tag, such as with:

button { background-color: $darkColor; }

This is deceptively complicated, as evidenced by this massive Github issue thread on vue-loader, which explores different approaches and issues. The solution that most people have landed on is modifying the vue.config.js file and tweaking the loaderOptions for CSS – there is a great guide by CSS-Tricks on how to do this here, and another guide by VueSchool here. Here is a sample vue.config.js file that follows their guide:

const path = require('path');

module.exports = {
    css: {
        loaderOptions: {
            sass: {
                data: `@import "@/styles/_variables.scss";`
            }
        }
    },
}

This solution works for many users, but those using third-party libraries formatted with Sass syntax, such as Vuetify, will likely end up with this dreaded error:

Error: Semicolons aren’t allowed in the indented syntax.

What is happening is that vue-loader/sass-loader is essentially trying to inject your import code, which is formatted as SCSS syntax (with a semicolon), into your third-party library, which uses SASS syntax (semicolons are not allowed.

Now what? We need some way to tell sass-loader to remove the semicolon when using with SASS syntax, and/or don’t inject it at all into .sass if possible.

Mixing Sass-syntax and SCSS-syntax in Vue

If you don’t need to use variables, and simply want some SCSS converted to CSS and applied across globally, across your entire app, you have a few options, some of which I’ve already mentioned:

  • Import in your main JS file (with “import ‘./styles/myStyle.scss'”)
  • Import in your root Vue template file (e.g. App.vue), either through the script section, the style section, or a style link tag.
  • Use your own pre-processor of choice to process your files and push them into the right file locations.

However, if you want to use variables, mixins, or anything else that needs to be “reference-able” across components, this won’t cut it.

To figure out how to get this to work, I started combing through a few relevant threads, starting with this Github issue for Vue-CLI. Some extra hints came from this, and especially this.

The solution(s):

We can tap into webpack settings, by using the “chainWebpack” function in our vue.config.js file, which is a way to modify the default behavior of Vue’s “behind-the-scenes” webpack configuration. Here are some relevant links:

Let’s get to work:

First: Make sure vue.config.js exists

Depending on how you use the Vue CLI to create your project, the vue.config.js file might not actually exist yet. If it does not, go ahead and create it in the root of your project.

Now that we have a config file, let’s move onto the solutions:

Solution – Option A – “Tap” into webpack settings using chainWebpack

This solution is provided by a Vuetify dev on a Github issue opened about this specific problem. I’ve modified it just a tiny bit to accept an array of files:

vue.config.js:

const globalSassFiles = [
    '~@/styles/_variables.scss',
    '~@/styles/App.scss'
]

module.exports = {
    css: {
        loaderOptions: {
            sass: {
                data: globalSassFiles.map((src)=>'@import "' + src + '";').join('\n')
            }
        }
    },
    chainWebpack: config => {
        ["vue-modules", "vue", "normal-modules", "normal"].forEach((match) => {
            config.module.rule('sass').oneOf(match).use('sass-loader')
                .tap(opt => {
                    return Object.assign(opt, {
                        data: globalSassFiles.map((src)=>'@import "' + src + '"').join('\n')
                    });
                });
        });
    }
}

This solution should work for most users, although a couple issues with it should be noted. For one, it injects your files into *every* used sass file, including all Vuetify files. This means that if you put something into Sass that is going to generate actual CSS code, it also ends up in all vendor CSS files. This also means an increased rebuild time for hot-reload, as touching one of those global sass files means all vendor files have to be reprocessed to re-inject your imports.

If you just need your Sass variables available to your own Vue template code, a better solution might be something like this:

const globalSassFiles = [
    '~@/styles/_variables.scss',
    '~@/styles/App.scss'
]

module.exports = {
    css: {
        loaderOptions: {
            sass: {
                data: globalSassFiles.map((src)=>'@import "' + src + '";').join('\n')
            }
        }
    },
    chainWebpack: config => {
        ["vue-modules", "normal-modules", "normal"].forEach((match) => {
            config.module.rule('sass').oneOf(match).use('sass-loader')
                .tap(opt => {
                    delete opt.data;
                    return opt;
                });
        });
    }
}

As you can see, “vue” has been removed from the match array, and for anything other than “vue” (template files), the import data is removed.

Solution – Option B – Use “Sass-Resource-Loader”

“Sass-Resource-Loader” is a loader plugin for Webpack that is specifically designed for injecting SASS variables as globals across imports. It even has a guide for using with vue.config.js and the Vue CLI 3+, which you can find here.

To get the same results as above, but with this solution, this is what my vue.config.js looks like:

const globalSassFiles = [
    './src/styles/_variables.scss',
    './src/styles/App.scss'
]

module.exports = {
    chainWebpack: config => {
        const oneOfsMap = config.module.rule('scss').oneOfs.store
        oneOfsMap.forEach(item => {
            item
                .use('sass-resources-loader')
                .loader('sass-resources-loader')
                .options({
                    resources: globalSassFiles
                })
                .end()
        })
    }
}

Final notes:

If none of this worked for you, my advice would probably be to either open a Github issue with Vue or Vue-loader, or use webpack outside of vue.config.js (either by switching to Vue CLI 2, which exposes more webpack stuff, or by setting up webpack from scratch outside of the Vue CLI). A great tool to use to try and determine how Vue is internally using Webpack is the “vue inspect” command.  For example, to see what webpack config is used for production builds, and save it to output.txt, use:

vue inspect -v --mode production > output.txt

Finally, on a personal note, I would have to say that this whole post exemplifies one of the problems with “magic” tools like the Vue CLI, which abstract away how things are actually working below the surface. It really shouldn’t be this difficult to tweak behavior, but it is. There is a great related thread on the balance between ease of use and exposing controls when it comes to the Vue CLI, and this reponse in particular is something that resonates with me.

Leave a Reply

Your email address will not be published. Required fields are marked *