Theming in Vue single file components

There are situations where it’s beneficial to build different CSS files for the same web app. An often seen example is theming an application. When you’re using Vue with its single file component Webpack loader, you’re in luck! You get a lot of flexibility that makes it straightforward to build such a feature.

Goal

The goal of the running example is to create a themed Vue app. Each Vue component can specify one or more theme styles. A build should only bundle one of these style themes together with the common styles.

Implementation

Let’s start with a basic Vue project, created with the Vue command line interface:

vue create themed-app

Now let’s open up App.vue and add two theme styles for this component, called theme “a” and “b”. We’ll add extra root <style> tags for these themes. Note the theme attribute which differentiates the tags from the common style.

The component renders a couple of Header.vue components. Let’s create it and give it some extra <style> tags for theme “a” and “c”.

Note that those styles declare scoped. Scoped means all styles get a component-specific classname to prevent name clashing. The scoped functionality should still work when implementing theme support.

If you would run a Vue build now, all style tags end up in the final CSS file (which is the default behavior). This is of course not what we want!

A solution can be to write a preprocessor before processing the files with Webpack. Or you could write your own Webpack loader to bundle the correct styles.

We’ll go with a rather elegant solution hidden in Webpack and vue-loader. vue-loader transforms each .vue file into one or more resources of a specific type. This is based on the blocks defined in each file. If you define a <style> tag, this creates a CSS resource stream. The resource is processed further as specified by the Webpack rules for CSS resources.

vue-loader will transform attributes set on tags to resource query parameters. This is implictly documented. Let’s see if we can use those parameters to filter specific styles for a theme bundle.

First, we need to know which Webpack rules apply to CSS resources. We can use the Vue CLI again:

vue inspect --rule css

This gives the following Webpack configuration blurb (shortened for brevity):

/* config.module.rule('css') */
{
test: /\.css$/,
oneOf: [
/* config.module.rule('css').oneOf('vue-modules') */
/* config.module.rule('css').oneOf('vue') */
/* config.module.rule('css').oneOf('normal-modules') */
/* config.module.rule('css').oneOf('normal') */
]
}

So there are 4 rules for CSS resource processing, each with a specific purpose. The oneOf parent rule makes sure that only one of these rules matches. Matching performs from top to bottom.

By the way, the default Vue CLI app also has out of the box rules configuration for postcss, scss, sass, less and stylus, which we also need to take in account.

Remember that we defined an “a”, “b” and “c” theme? Let’s say we only want to match theme “a” and the common style resources. We can get the desired result by nullifying theme styles “b” and “c” before any other matching.

We make this work by writing a resourceQuery condition. As the docs state, a condition can be (among other things) an object with multiple matching conditions. We can use it to define the following pseudocode filter:

If the resource query contains theme=b or theme=c, stop parsing the resource and throw away the result.

Luckily, there’s a loader out there built for nullifying results: null-loader. If we could change the Webpack config into the following, we get really close to our original goal.

/* config.module.rule('css') */
{
test: /\.css$/,
oneOf: [
/* config.module.rule('css').oneOf('nullify-other-themes') */
{
resourceQuery: {
or: [
/theme=b/,
/theme=c/
]
},
use: [
{
loader: 'null-loader'
}
]
},
/* config.module.rule('css').oneOf('vue-modules') */
/* config.module.rule('css').oneOf('vue') */
/* config.module.rule('css').oneOf('normal-modules') */
/* config.module.rule('css').oneOf('normal') */
]
}

By default, an app built with the Vue CLI keeps a layer between the project and the Webpack configuration. There’s no way to ‘eject’ the Vue configuration and change the Webpack config yourself. You need to use the APIs that Vue gives you.

If you want to change the Webpack config, there are many ways of changing the out of the box configuration. The most powerful way to mutate configuration is by defining a chainWebpack function in thevue.config.js file. This gives you access to the chainable API to mutate the config.

Yet, prepending a oneOf rule is currently more complex than it needs to be due to a bit of missing API of webpack-chain. After some tinkering, we can get this to work (and we can wait for the pull request to land to add the extra API to fix this).

We still need to decide how to define the selected theme for the current build. I settled with an environment variable that you can change, I’ll leave it up to the reader to think of alternative ways.

Why not bundle many CSS files with one build? This is quite hard, as a single Webpack configuration functions on a single graph of (conditional) dependencies. And because the Vue CLI doesn’t support building multiple Webpack configurations, this is something that we need to fix outside of the build.

Results

Now that we have our Vue theming support in place, we can start the dev server:

yarn serve
// or
VUE_THEME=a yarn serve

And see that we get the common style applied plus the first theme (“a”).

End result (theme “a”)

If we start with a specific theme:

VUE_THEME=b yarn serve

or

VUE_THEME=c yarn serve

we get different results:

End result (theme “b”)
End result (theme “c”)

You can create different production builds for all three themes:

VUE_THEME=a yarn build
VUE_THEME=b yarn build
VUE_THEME=c yarn build

Make sure to copy the generated CSS before kicking off the next build, as the out of the box setup clears the dist folder.

Conclusion

I hope you like the proposed implementation of theming support for Vue. Thanks to Webpack’s flexibility and the lesser known features of vue-loader, we have very powerful ways of combining different styles into the final CSS bundle.

You can find the final code repo here.