Build a custom chat launcher
Replace the default round launcher with your own button. Useful for marketing pages with a hero CTA, navbar entry points, or pricing pages where you want a Talk to sales link to open chat directly.
1. Hide the default launcher
Add the data-no-launcher attribute on the script tag. The bundle still loads and the chat panel still works; we just do not render the default button.
<script
src="https://app.message.com/widget.js"
async
data-workspace="YOUR_WORKSPACE_ID"
data-no-launcher="true"
></script>2. Add your own button
Build any button you want. Hook its click handler to Message.toggle() (or open() / close() if you prefer one-way controls).
<button id="chat-cta" type="button">
Talk to support
<span id="chat-unread" hidden>0</span>
</button>
<script>
const btn = document.getElementById("chat-cta");
const badge = document.getElementById("chat-unread");
btn.addEventListener("click", () => window.Message?.toggle());
window.MessageOnReady = function () {
window.Message.on("unread:changed", ({ count }) => {
badge.textContent = String(count);
badge.hidden = count === 0;
});
};
</script>Message.toggle(), Message.open(), and Message.close() are part of the JavaScript API. Calling them before the bundle has loaded is a no-op; use MessageOnReady for handlers that depend on the API being ready.
3. Show the unread count
Subscribe to unread:changed. The widget emits this whenever new agent messages arrive while the panel is closed.
4. Animate state changes
Listen for open and close events to flip a class on your button:
window.Message.on("open", () => btn.classList.add("chat-open"));
window.Message.on("close", () => btn.classList.remove("chat-open"));5. Open with a topic preset
Common pattern: a pricing-page button that opens chat and seeds the first message:
btn.addEventListener("click", () => {
window.Message?.open();
window.Message?.sendMessage("Hi, I have a question about pricing.");
});Common pitfalls
- Calling Message before ready. Use
MessageOnReadyorwindow.Message?.toggle()with the optional-chaining guard. Otherwise the click handler can fire before the bundle exists. - Multiple buttons. Several buttons can toggle the same panel.
Message.toggle()is idempotent. - SPA route changes. If the button unmounts and remounts on navigation (React, Vue), rebind the click handler each mount.
- Accessibility. Your custom button needs the same accessibility affordances the default has: focusable,
aria-label, visible focus ring. The default launcher meets WCAG AA; replacements often regress.