Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

1.1K
Views
How do I create a Vue 3 custom element, including child component styles?

I tried Vue's defineCustomElement() to create a custom element, but the child component styles are not included in the shadow root for some reason.

I then tried to manually create my shadow root using the native Element.attachShadow() API instead of using defineCustomElement() (based on a Codesandbox), but then no styles were loaded at all:

Code: main.js:

import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";

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

createApp(App).use(store).use(router).mount(holder);

Code vue.config.js:

module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule("vue")
      .use("vue-loader")
      .loader("vue-loader")
      .tap((options) => {
        options.shadowMode = true;
        return options;
      });
    config.module
      .rule("css")
      .oneOf("vue-modules")
      .use("vue-style-loader")
      .tap((options) => {
        options.shadowMode = true;
        return options;
      });
    config.module
      .rule("css")
      .oneOf("vue")
      .use("vue-style-loader")
      .tap((options) => {
        options.shadowMode = true;
        return options;
      });
  },
};

Code package.json:

{
  "name": "shadow-root",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build"
  },
  "dependencies": {
    "vue": "^3.2.20",
    "vue-loader": "^16.8.2",
    "vue-router": "^4.0.0-0",
    "vue-style-loader": "^4.1.3",
    "vuex": "^4.0.0-0"
  },
  "devDependencies": {
    "@vue/cli-plugin-router": "~4.5.0",
    "@vue/cli-plugin-vuex": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "node-sass": "^4.12.0",
    "sass-loader": "^8.0.2"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}

How I can create a custom element with all its styles in the shadow root?

over 4 years ago · Santiago Trujillo
1 answers
Answer question

0

That Vue config is not necessary in Vue 3. It was only needed by the dev server in Vue 2 to render the styles in custom elements.

Using defineCustomElement() is the recommended way to register custom elements. However, there's an open issue when using defineCustomElement(), where nested component styles are not rendered at all (@vuejs/vue-next#4462).

A workaround is to import all components as custom elements so that the styles are attached to the component definition instead of being appended to <head>, then insert those styles into the DOM upon mounting:

  • Enable vue-loader's customElement mode in vue.config.js:

    // vue.config.js
    module.exports = {
      chainWebpack: config => {
        config.module
          .rule('vue')
          .use('vue-loader')
          .tap(options => {
            options.customElement = true
            return options
          })
      }
    }
    

    Alternatively, rename all component filename extensions from .vue to .ce.vue.

  • Create a utility function that wraps Vue's defineCustomElement() and does the following in a setup():

    1. Create a temporary application instance that adds a mixin for the mounted and unmounted lifecycle hooks.
    2. In the mounted hook, insert the component's own styles from this.$.type.styles into the DOM in a <style> tag. Do the same with the component definitions from the this.$options.components map.
    3. In the unmounted hook, remove the <style> tag that was inserted from mounted.
    4. Copy the temporary application instance's _context into the current application context from getCurrentInstance().
    5. Return a render function for the component.
    // defineCustomElementWithStyles.js
    import { defineCustomElement as VueDefineCustomElement, h, createApp, getCurrentInstance } from 'vue'
    
    const nearestElement = (el) => {
      while (el?.nodeType !== 1 /* ELEMENT */) el = el.parentElement
      return el
    }
    
    export const defineCustomElement = (component) =>
      VueDefineCustomElement({
        setup() {
          const app = createApp()
          1️⃣
          app.mixin({
            mounted() {
              const insertStyles = (styles) => {
                if (styles?.length) {
                  this.__style = document.createElement('style')
                  this.__style.innerText = styles.join().replace(/\n/g, '')
                  nearestElement(this.$el).prepend(this.__style)
                }
              }
    
              2️⃣
              insertStyles(this.$?.type.styles)
              if (this.$options.components) {
                for (const comp of Object.values(this.$options.components)) {
                  insertStyles(comp.styles)
                }
              }
            },
            unmounted() {
              this.__style?.remove() 3️⃣
            },
          })
    
          4️⃣
          const inst = getCurrentInstance()
          Object.assign(inst.appContext, app._context)
    
          5️⃣
          return () => h(component)
        },
      })
    
  • Edit public/index.html to replace the <div id="app"> with a custom element (e.g., named "app-root"):

    Before:

    // public/index.html
    <body>
      <div id="app"></div>
    </body>
    

    After:

    // public/index.html
    <body>
      <app-root id="app"></app-root>
    </body>
    
  • Instead of createApp(), use the defineCustomElement() from above to create a custom element of your app:

    Before:

    // main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    createApp(App).mount('#app')
    

    After:

    // main.js
    import { defineCustomElement } from './defineCustomElementWithStyles'
    import App from './App.vue'
    customElements.define('app-root', defineCustomElement(App))
    

demo

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!