Your product runs on Vue 3, Vite builds it in seconds, and your support channel is still a mailto link buried in the footer. Every question from a trial user lands in a personal inbox, threads get lost, and half of them never get answered. Adding live chat fixes that, and in a Vue SPA it is genuinely a five minute job if you know one thing about how widgets and vue-router interact.
This guide covers plain Vue 3 with Vite. Two install options, the single-page-app nuance that trips people up, and how to switch on AI replies once the widget is live.
What you are actually installing
A chat widget is not a Vue component. It is a small script that injects its own UI into the page, outside your app's root element. muro's loader is about 6 KB, loads async, and attaches the chat bubble to the document itself, not to your #app div. That detail matters later.
- →No npm package needed. The widget is a script tag, so there is nothing to add to
package.jsonand nothing that bloats your bundle. - →Async by default. The loader does not block rendering and does not hurt your Lighthouse score.
- →Framework-agnostic. The same snippet keeps working if you later migrate to a meta-framework or bolt Vue onto a Rails or Django backend.
Option 1: paste the snippet into index.html (simplest)
In a Vite project, index.html lives at the project root, not inside public/. It is the actual entry point Vite serves and transforms, so anything you put in its <head> ships with every page. Open it and paste the snippet just before the closing </head> tag:
html<script>
(function(w,d,s,o){
w.MuroChat=o;w[o]=w[o]||function(){(w[o].q=w[o].q||[]).push(arguments)};
var j=d.createElement(s);j.async=1;j.src='https://muro.chat/widget.js';
d.getElementsByTagName('head')[0].appendChild(j);
})(window,document,'script','muro');
muro('init',{widgetId:'YOUR_WIDGET_ID'});
</script>Replace YOUR_WIDGET_ID with the ID from your muro dashboard (create a site at signup and the snippet with your real ID is on the install screen). Run npm run dev, open the app, and the bubble appears in the corner. That is the whole install.
Because index.html is static, this option has one limitation: the widget ID is hardcoded. If you run separate staging and production sites in muro and want a different ID per environment, use option 2.
Option 2: load it from App.vue with onMounted
If you would rather keep third-party scripts in code, or you need the widget ID to come from an environment variable, load the widget from your root component. App.vue mounts exactly once per page load, which makes it the right home. Do not put this in a route component.
js// App.vue, inside <script setup>
import { onMounted } from 'vue'
onMounted(() => {
if (window.muro) return // guard against double-init (HMR, remounts)
;(function (w, d, s, o) {
w.MuroChat = o
w[o] = w[o] || function () { (w[o].q = w[o].q || []).push(arguments) }
const j = d.createElement(s)
j.async = true
j.src = 'https://muro.chat/widget.js'
d.getElementsByTagName('head')[0].appendChild(j)
})(window, document, 'script', 'muro')
window.muro('init', { widgetId: import.meta.env.VITE_MURO_WIDGET_ID })
})Then put the ID in your env files so each environment gets its own site:
bash# .env.production
VITE_MURO_WIDGET_ID=your_production_widget_id
# .env.development
VITE_MURO_WIDGET_ID=your_staging_widget_idThe if (window.muro) return guard is doing real work. Vite's hot module replacement can re-execute component setup during development, and without the guard you would stack duplicate init calls. In production it is harmless insurance.
The SPA nuance: init once, and it survives navigation
Here is the part generic install guides get wrong for single-page apps. Vue-router navigation does not reload the page. When a user clicks from /dashboard to /settings, vue-router swaps components inside your app root and updates the URL. No new document, no new <head>, no scripts re-run.
That is exactly why the widget should be initialized once, at the document level, and never inside a routed view:
- →The widget lives on `document`, not in your component tree. It attaches its bubble outside
#app, so vue-router swapping views never touches it. An open conversation stays open while the user navigates. - →Initializing in a route component is the classic mistake. Put the loader in
HomeView.vueand users who land directly on/pricingnever get the widget, while navigating back to home risks a second init. - →Conversations persist across the session. The visitor's thread carries across route changes and page refreshes, so nobody has to type their question twice.
One honest caveat: this guide is for a plain client-rendered Vue 3 SPA served by Vite. If your app is server-rendered, the same principle applies (init once, on the client, after mount), but where the code goes differs per framework, so follow the conventions of whatever is doing your SSR.
Make it look like your product, not a bolt-on
Out of the box the widget is deliberately neutral. In the muro dashboard you can set:
- →Color, so the bubble matches your brand instead of screaming third-party.
- →Position and size, bottom-right or bottom-left, compact or roomy.
- →Language, with 12+ built in. If your Vue app serves French or German users, the widget chrome can match.
- →Agent name and avatar, so replies come from a person (or a named bot), not from a generic 'Support'.
None of this requires touching your Vue code again. Change it in the dashboard and it applies on the next load.
Turn on AI replies, and know when they stop
A chat bubble nobody answers is worse than no chat bubble. The realistic setup for a small team is AI for the repetitive layer and a human for everything that actually matters.
muro's AI auto-reply is grounded on content you give it. Point it at your docs or FAQ by importing a URL, add a little product context, and it answers the questions that fill most inboxes: how do I reset a password, is there an API, where do I find my invoice. Grounding is the important word. The AI answers from your published material instead of improvising.
Just as important is when it does not answer. muro hands the conversation to a human when the AI is unsure, and for the categories where a wrong answer costs you: refunds, account issues, or a visitor who is clearly angry. The handoff shows up in the shared inbox with the full conversation attached, so you reply with context instead of starting cold.
On cost: both plans include managed AI credits (1,000 per month on Solo, 3,000 on Fleet, one credit per AI action), and if you would rather run unlimited AI you can paste your own Claude API key. BYOK runs at zero markup. Plans are $19 and $59 with unlimited agents on both, no per-seat pricing; the details are on the pricing page.
After the install: answer from one place
The widget is the visible half. The half that keeps you sane is where messages land. Every conversation from your Vue app goes into a shared inbox, so a cofounder or a first support hire can jump in without forwarding threads around. Saved replies handle the answers you type weekly, and automation rules can tag, route, or auto-close the noise before you ever see it.
Two more pieces worth wiring up on day one. Email forwarding turns your existing support@ address into the same inbox, and reply-by-email means you can answer chat messages from your phone without opening a dashboard. And if you want conversations flowing into your own tooling, there is a full REST API with webhooks; the API docs cover both.
A five minute checklist before you ship
- 01Paste the snippet (option 1) or the loader (option 2) and confirm the bubble renders locally.
- 02Click through three or four routes and confirm the widget persists and an open conversation stays open.
- 03Send yourself a test message and answer it from the muro inbox.
- 04Import your docs URL, then ask the widget a question your docs answer. Check the reply matches reality.
- 05Ask something it should not handle ('I want a refund') and confirm it hands off to you instead of guessing.
If all five pass, deploy. The widget is the same static script in production, so there is nothing environment-specific to break, unless you went with env-based IDs, in which case double check that .env.production made it into your build.
That is the whole job. Create a site in muro (there is a 14-day free trial, no card), paste the snippet into index.html or drop the loader into App.vue, and point the AI at your docs. If you want to poke at a working setup first, the live demo shows the widget and the inbox side by side. Your Vue app gets a support channel that actually gets answered, and you get out of the mailto business.