Using Vue Single-File Components Inside Shadow DOM

Let’s say you’re building a tiny little Vue app. Not a full-on single page app, but something very tiny that will need to be embedded into other pages. Like a fully interactive widget that can do a wide variety of things, but will need to be self-contained so as not to interfere the rest of the page.

Traditionally, in the past, we did this with a wide variety of approaches. Going back to the 90s, we use Java Applets (remember those?) and Active-X controls (ugh). We used Flash too (double ugh). Lately the preferred approach has been iframes, and while this is still a perfectly valid approach, it has it’s own set of problems.

But now, we also have Shadow DOM which provides us another approach to building richly interactive widgets that are (mostly) contained from interfering with the styling of the surrounding page and, crucially, doesn’t allow the surrounding page to interfere with the widget!

And, yes, Vue can totally be used inside a shadow tree. It just take a bit of setup work.

To start, I am going to assume you have some familiarity with modern web development tools. That means you at least somewhat understand how tools like webpack, VueJS, and Vue Single-File Components work. If you’re not familiar with these tools, please take some time to read up on them.

The Problem

Let’s assume you’ve built a simple Vue components that looks something like this. Let’s also assume you have webpack and all the other things configured to compile the components and most of that already working.

<template>
    <div class="foo">
        <button @click="doThings" class="bar">Baz</button>
    </div>
</template>

<script>
export default {
  methods: {
    doThings() {
      alert("I did something!");
    }
  }
}
</script>

<style lang="scss" scoped>
.foo {
    .bar {
        display: block;
        background-color: red;
    }
}
</style>

One thing to keep in mind when building something like this is that every instance of a Vue component is also an instance of Vue. So you can render this into an HTML element of your choice.

<div class="container"></div>
import vue from 'vue';
import MyComponent from 'src/MyComponent';

let app = new Vue({
  el: document.querySelector(".container"),
  render: h => h(MyComponent, {})
});

Now, this will work just beautifully. You should see your Vue app mount onto the .container div and display the button. You should be able to click on it and have it do something.

But what happens if you do this?

<div class="container"></div>
<style>
button {
    border: 1px solid green;
}
</style>

You now have a green border on your button despite using scoped styles on your component. This is because the styles from the page are leaking into the component. This is actually by design and, in most cases, a good thing. It allows you to have a single stylesheet (or multiple stylesheets compiled into a single one) that wraps up common styles so that your scoped styles in components can be as few as possible.

But in this case, that’s not what we want. We want our component to only have the styles we want without the page styles leaking in. How do we do that?

Enter Shadow DOM

Shadow DOM gives us that ability. Think of Shadow DOM as an isolated, independent DOM tree attached to an existing element inside the current tree. Styles in the main tree do not leak into any shadow trees, and styles in those shadow trees are independent of the main tree and, importantly, independent of other shadow trees. Basically, each shadow tree is an independent tree with its own styling.

So with Shadow DOM, things get a little more complicated. Creating a shadow root is actually fairly straightforward. We might modify our code to look something like this:

import vue from 'vue';
import MyComponent from 'src/MyComponent';

let treeHead = document.querySelector(".container");
let holder   = document.createElement("div");
let shadow   = treeHead.attachShadow({mode: 'open'});
shadow.appendChild(holder);

let app = new Vue({
  el: holder,
  render: h => h(MyComponent, {})
});

Run that, and your element is now inside it’s own isolated shadow root!

… But it probably looks like garbage, and this is because the styles that you created on the single file component itself are not available in the shadow tree. They are only available in the main DOM tree. And to understand why this is happening, you need to understand how webpack’s style-loader works.

Style Loading

By default, the style-loader appends <style>/<link> elements to the end of the style target, which is the <head> tag of the page unless specified by insert. So by default, styles that webpack’s style-loader has pulled out of the Vue Single-File Component get put in the <head> tag. Again, this is a reasonable default for people building SPAs, but it’s not what we need. Fortunately, style-loader is flexible for other use cases.

In particular, style-loader allows you to use a function to determine where to put the style tags. And, this is important, that function is executed in the context of the page.

So, you might modify your webpack config file to do this:

  module: {
    rules: [
      {
        test: /\.s[ac]ss$/,
        use: [
          {
            loader: "style-loader",
            options: {
              insert: require("./css-loader-shim")
            },
          },
          "css-loader",
          {
            loader: "sass-loader",
            options: {
              implementation: require("sass"),
              sassOptions: {
                fibers: require("fibers"),
              }
            }
          }
        ]
      },
    ]

The important thing there is where we changed style-loader with an options object that has an insert property. That will load our loading shim. You could just add a closure right there in the webpack file, but I think it’s probably better to separate page logic from configuration and building as much as possible. Remember, that closure is executed on the page.

And create a css-loader-shim.js that looks like this:

module.exports = function(elem) {
  window["foo"] = window["foo"] || {};
  window["foo"].loadStyles = function(el) {
    el.appendChild(elem);
  }
}

So when the compiled Javascript is brought into the page, it will immediately execute that function with elem containing the style or link tag. But we don’t do anything with that immediately. Instead, we create a callback that we can call when we are ready to insert the styles at a location of our choosing. And that means inserting them into the shadow root!

As a quick note, this function is intentionally simple. It assumes you are only loading one file. If you are going to be loading multiple files (such as from using code splitting), that function will need to take into account which element is calling it and put the styles in the right location. This is meant to illustrate this approach, not be a be-all, end-all solution.

So finally, we’ll modify our application code above again:

import vue from 'vue';
import MyComponent from 'src/MyComponent';

let treeHead = document.querySelector(".container");
let parent   = document.createElement("div");
let holder   = document.createElement("div");
let shadow   = treeHead.attachShadow({mode: 'open'});

foo.loadStyles(parent);
parent.appendChild(holder);
shadow.appendChild(parent);

let app = new Vue({
  el: holder,
  render: h => h(MyComponent, {})
});

See what we did there? We called foo.loadStyles(parent) on a div we created, then inserted all that into the shadow root. So if you reload, now you should have a red button with no green border. And any buttons you had on the page would also not be red even if you applied the right classes. Everything from a style standpoint is isolated to that spot.

Browser Support

As of the writing of this post this approach is supported in 90% of browsers. Firefox and Chrome have supported it for years, and mobile browser support is good. Basically the only things that don’t support this approach are really old versions of Internet Explorer and pre-Chromium Microsoft Edge, as well as a smattering of other minor browsers. So it should be fairly safe to use.

However, if you need to support these ancient browsers, there is a shadow DOM polyfill that you can include as well.

Did something I wrote help you out?

That's great! I don't earn any money from this site - I run no ads, sell no products and participate in no affiliate programs. I do this solely because it's fun; I enjoy writing and sharing what I learn.

COVID-19 has taken the world by storm and left a lot of brokenness in its wake. A lot of people are suffering. If you feel so inclined, please make a donation to your local food bank or medical charity. Order take-out from your local Chinese restaurant. Help buy groceries for an unemployed friend. Help people make it through to the other side.

But if you found this article helpful and you really feel like donating to me specifically, you can do so below.

Read More

Responsive CSS3 Columns with Sass and Bootstrap

Impatient? Scroll to the bottom to download. So I recently was working on a site and wanted to use CSS3 columns. But I really like how the grid system works in Bootstrap, and wanted to be able to define columns in a similar way (i.e. have different number of columns depending on the screen size). Not finding any pre-cooked versions, I decided to write my own. Strictly speaking, you don’t need Bootstrap for this to work. But I did re-use Bootstrap’s grid variables so that it breaks along the same lines that Bootstrap’s grid does. It’s also worth noting that, natively, the columns will collapse on their own if you specify a width. This method just gives you a bit more control.

Renaming Grunt NPM Tasks

For the last few years, Gulp has been my go-to task runner for Node projects and, generally, anywhere where I need to build things or run tasks. But the recent release of Gulp 4 broke all of my config files and left me with hours of frustrating rewrites, I decided to see what else might be out there. And, naturally, I landed on Grunt. One thing I liked about Gulp (prior to 4.0) was it’s much looser structure that allowed a lot of freedom in how you structured your file. Grunt seems to be much more structured and opinionated. And sometimes, I don’t like those opinions. A prime example of this is grunt-contrib-watch. When I type grunt watch, I want to run a series of setup tasks first before firing the watcher up. But grunt-contrib-watch squats on the prime real estate that is the watch command. But I wanted to use that command. And there doesn’t seem to be any way to just say “run these arbitrary tasks before starting the watcher.” At least not one that I could find clearly documented. Sure, I could just make my own mywatch or similar command, but I’m picky. I want my command, so we need a way to rename it.