Skip to content

Vue

mountly-vue is the Vue 3 adapter. It exposes the same surface as the React adapter, createWidget(Component, { styles }), over a different rendering layer.

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 scopes the styles per component, so collisions are rare. Pass shadow: true if you need a hard boundary, such as CMS embeds or third-party hosts. The 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 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, since 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 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 });