Skip to content

Vue

mountly-vue is the Vue 3 adapter. Same surface as the React adapter — createWidget(Component, { styles }) — different rendering layer underneath.

Terminal window
pnpm add mountly mountly-vue vue
signup-card.ts
import { createWidget } from "mountly-vue";
import SignupCard from "./SignupCard.vue";
export default createWidget(SignupCard);

By default the widget mounts in light DOM. The adapter fetches the sibling dist/index.css (extracted by Vite’s Vue plugin from <style scoped>) and applies it before render. Vue’s data-v-… hash already scopes the styles per component, so collisions are rare. Pass shadow: true if you need a hard boundary (CMS embeds, third-party hosts) — styles are then scoped twice, once by Vue and once by the shadow root.

Want to inline the CSS string instead? That’s still fine:

import styles from "./SignupCard.css?inline";
export default createWidget(SignupCard, { styles });
import widget from "./signup-card.js";
widget.mount(document.querySelector("#cta-mount"), {
headline: "Try the API",
plan: "pro",
});

Each mount() unmounts any existing Vue app in the same container first.

Under the hood, the adapter calls createApp({ render: () => h(Component, props) }) and mounts that into the container (shadow root if shadow: true, otherwise light DOM). There is one Vue app per container.

import { createOnDemandFeature } from "mountly";
const signup = createOnDemandFeature({
moduleId: "signup-card",
moduleUrl: "/widgets/signup-card/dist/index.js",
});
signup.attach({ trigger: btn, preloadOn: "hover", activateOn: "click" });

mountly threads moduleUrl through to the adapter, which fetches dist/index.css (sibling) and applies it before render. Use loadModule / render only when you need bespoke behaviour.

Both work. The adapter doesn’t care which API style your component uses — it just calls createApp with the imported component.

Each widget mount is its own Vue app. Providers do not cross widgets. If two widgets need to share state, either:

  • Wrap them in one outer Vue app and use mountly only for the load lifecycle (you mount the outer app yourself).
  • Use a framework-agnostic store (Pinia with a global instance, or any import-level singleton).

mountly init --vue (when supported by the CLI version you have) configures tsup to emit:

  • dist/index.js — bundles Vue.
  • dist/peer.js — externalises Vue. Pairs with an import map.

Multi-widget hosts should use the peer build to share one Vue copy. See Distribution.

Vue SFC <style scoped> works inside the widget. With shadow: true the shadow root adds another layer of isolation; without it, Vue’s data-v-… hash is usually enough. If you’re using global utility classes (Tailwind), those go into styles (or a host <link>) so the widget can reach them.

Same hazard as React: two widgets with dist/index.js give you two Vue runtimes on the same page. Use the peer build for multi-widget pages.

Vite’s Vue plugin handles .vue files in the widget build. If you’re running tsup directly, install unplugin-vue or a similar SFC compiler — mountly-vue does not ship one.

import { createWidget } from "mountly-vue";
import Component from "./Component.vue";
export default createWidget(Component);

The Vite Vue plugin extracts <style scoped> blocks into dist/index.css. The adapter picks it up automatically when the host passes moduleUrl.

Compile Tailwind into the same dist/index.css and the zero-config flow still applies. If you prefer to ship the CSS as an inline string:

import { createWidget } from "mountly-vue";
import Component from "./Component.vue";
import styles from "./styles.css?inline";
export default createWidget(Component, { styles });