For AI agents: Find a documentation index at https://talkjs.com/llms.txt (full content at https://talkjs.com/llms-full.txt). Get a markdown version of any page by appending .md to its URL path.
Skip to content

Vue.js chat

Vue chat components for Vue 3 and Nuxt

TalkJS gives your Vue app a chat UI and the backend behind it. Vue needs one compiler rule so it treats the chat tags as browser elements: limit it to tags that start with t-. The broader rule, every tag with a dash, quietly breaks your kebab-case components. In Nuxt, the same rule lets the server render the chat.

// vite.config.js, inside your vue() call
vue({
  template: {
    compilerOptions: {
      // TalkJS tags only, so <router-view> still renders
      isCustomElement: (tag) => tag.startsWith('t-'),
    },
  },
}),
Nina
Hi Frank! Did the chat show up on the orders page?
9:15 AM
You
Yes, and the router links work again too.
9:16 AM
Nina
Great, I’ll check it on my phone.
9:17 AM
thumbs-up
Write a reply…
You
Hey, I’m having trouble accessing my account. It says my password is incorrect.
5m ago
SupportBot
Hi there! No problem — let’s get that sorted. Have you tried resetting your password using the ‘Forgot password?’ link?
4m ago
thinking
thumbs-up
You
Yes, but I haven’t received the reset email yet.
3m ago
SupportBot
Thanks for confirming. I’ve just sent a new reset link to your email. Please check your inbox and spam folder.
2m ago
SupportBot
Is there anything else I can help you with?
just now
Got it — checking my inbox now!

Which tags should Vue treat as custom elements?

Only the ones that start with t-. The TalkJS chat UI is three web components, <t-chatbox>, <t-inbox> and <t-conversation-list>, and Vue’s compiler needs to know they’re browser elements, not Vue components. You tell it with isCustomElement, inside the vue() call that’s already in your vite.config.js:

plugins: [
  vue({
    template: {
      compilerOptions: {
        isCustomElement: (tag) => tag.startsWith('t-'),
      },
    },
  }),
  // ...the plugins you already have
],

Vue’s own guide shows a broader rule, tag.includes('-'). Don’t copy it. It matches every tag with a dash, so it also catches your own kebab-case components and the router’s ones, and they stop working with no warning in the console. The PascalCase <RouterView> that create-vue writes keeps working, so nothing breaks until the first person on your team writes kebab-case.

What each rule did to the same template

TagEvery dashOnly t-
<RouterView><RouterView> with includes('-'): RenderedRendered<RouterView> with startsWith('t-'): RenderedRendered
<router-view><router-view> with includes('-'): Empty tagEmpty tag<router-view> with startsWith('t-'): RenderedRendered
<router-link><router-link> with includes('-'): Plain textPlain text<router-link> with startsWith('t-'): RenderedRendered
<hello-world><hello-world> with includes('-'): Empty tagEmpty tag<hello-world> with startsWith('t-'): RenderedRendered
<t-chatbox><t-chatbox> with includes('-'): Live chatLive chat<t-chatbox> with startsWith('t-'): Live chatLive chat
One create-vue app with Vue 3.5.44 and vue-router 5.4.0, October 2026. Filled dots worked; empty ones broke.

With no rule at all, a Vite app still shows the chat, and Vue prints one warning in development: Failed to resolve component: t-chatbox. In Nuxt you shouldn’t skip the rule. The rule is the same in an Ionic Vue app.

Hosted chat or a Vue chat UI kit?

A Vue chat UI kit hands you Vue components for the message list, the bubbles and the input, and leaves storing and delivering the messages to you. Advanced Chat Components, an open-source kit, puts it plainly in its README: “Backend-agnostic — you own the data layer.” So you also build the server that stores and delivers messages in real time, then notifications for people who are offline, then moderation once strangers can message each other.

TalkJS is the chat UI and the backend in one. We store and deliver the messages, and every plan includes email, SMS and mobile push notifications and moderation tools. Development mode is free while you build. After launch you pay per monthly active user, from $279 a month for 10,000 users on Basic.

How do Vue refs drive the chat?

Like any other prop. Bind :conversation-id to a ref and the chatbox switches conversations the moment the ref changes, with no page reload. Pass an object, such as theme-custom, and it arrives as an object. You don’t need the .prop modifier.

The same session can feed your own UI. The component below keeps a count of unread conversations in a ref. In our test app it went from 0 to 1 within 5 seconds of a new message.

npm install @talkjs/web-components @talkjs/core
<script setup>
import { ref, onUnmounted } from 'vue';
import '@talkjs/web-components';
import '@talkjs/web-components/default.css';
import { getTalkSession } from '@talkjs/core';

const appId = '<APP_ID>';
const session = getTalkSession({ appId, userId: 'frank' });

// Our test app was empty, so this creates the users and conversations.
session.currentUser.createIfNotExists({ name: 'Frank' });
session.user('nina').createIfNotExists({ name: 'Nina' });
session.user('sam').createIfNotExists({ name: 'Sam' });
const first = session.conversation('my_conversation');
first.createIfNotExists();
first.participant('nina').createIfNotExists();
const second = session.conversation('vue_second');
second.createIfNotExists({ subject: 'Second conversation' });
second.participant('sam').createIfNotExists();

const conversationId = ref('my_conversation');
function switchConversation() {
  conversationId.value =
    conversationId.value === 'my_conversation' ? 'vue_second' : 'my_conversation';
}

// Starts with the 20 most recently active conversations; call loadMore for more.
const unreadCount = ref(0);
const unreads = session.subscribeConversations(
  { filter: { isUnread: true } },
  (conversations) => { unreadCount.value = conversations.length; },
);
onUnmounted(() => unreads.unsubscribe());
</script>

<template>
  <button @click="switchConversation">Switch conversation</button>
  <p>Unread conversations: {{ unreadCount }}</p>
  <t-chatbox
    style="width: 100%; max-width: 400px; height: 600px;"
    :app-id="appId"
    user-id="frank"
    :conversation-id="conversationId"
  />
</template>

Built from the code example in the TalkJS Vue guide. Versions: Vue 3.5.44, Vite 8.3.5, @talkjs/web-components 0.3.12 and @talkjs/core 1.12.1, October 2026.

Before launch, have your server sign a token for each user and turn on authentication in your dashboard, as the authentication docs describe.

Adding TalkJS chat to Nuxt

Why does my Nuxt page return a 500?

Because a top-level import of the web components runs on the server, where there’s no browser. In a fresh Nuxt 4.6.3 app, nuxt dev answered ReferenceError (500): HTMLElement is not defined.

Import the components in onMounted, which only runs in the browser, and start the session there too. This is app/pages/index.vue:

<script setup lang="ts">
import { getTalkSession } from '@talkjs/core';

const appId = '<APP_ID>';
const userId = 'frank';
const otherUserId = 'nina';
const conversationId = 'my_conversation';

onMounted(async () => {
  await import('@talkjs/web-components');

  const session = getTalkSession({ appId, userId });
  session.currentUser.createIfNotExists({ name: 'Frank' });
  session.user(otherUserId).createIfNotExists({ name: 'Nina' });
  const conversation = session.conversation(conversationId);
  conversation.createIfNotExists();
  conversation.participant(otherUserId).createIfNotExists();
});
</script>

<template>
  <main>
    <t-chatbox
      style="width: 100%; max-width: 400px; height: 600px;"
      :app-id="appId"
      :user-id="userId"
      :conversation-id="conversationId"
    />
  </main>
</template>

Where does the t- rule go in Nuxt?

In nuxt.config.ts, under vue.compilerOptions, with the TalkJS stylesheet in css. Without the rule, the server rendered an empty comment where the chat goes and Vue logged a hydration node mismatch. With the rule, the server HTML included the real <t-chatbox> tag with its attributes, the browser turned it into a live chat once the import finished, and the console stayed clean.

export default defineNuxtConfig({
  css: ['@talkjs/web-components/default.css'],
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith('t-'),
    },
  },
})

Do I need <ClientOnly> or a plugin?

Neither. You could use a .client.ts plugin, but it puts the chat code in the entry chunk that every page loads. With the import in onMounted, the chat chunk loads only on the page with the chat.

JavaScript a Nuxt page loads, by where you import TalkJS

Plugin (.client.ts)
Plugin (.client.ts): 1,286 kB on every page1,286 kB on every page
Import in onMounted
Import in onMounted: 385 kB on every page, plus 903 kB on the chat page385 kB on every page, plus 903 kB on the chat page
0700 kB1,400
Minified JavaScript from one nuxt build each, Nuxt 4.6.3 with @talkjs/web-components 0.3.12, October 2026. For the onMounted build, “on every page” counts the two chunks Nuxt preloaded.
Retreat Guru logo
We were able to offload all messaging to TalkJS — hugely simplifying our development of an otherwise complex stack.
Deryk Wenaus
CTO, Retreat Guru

Ready-made chat UI you can customize

You style the pre-built chat UIs with properties, CSS or a custom theme, as the web components docs show. Need a chat popup? Open a chatbox from a button of your own.

Inbox

The TalkJS Inbox shows the chat history on the left, selected conversation on the right. It’s designed to act as the messaging center of your app, usually living on its own page.

Chatbox

The TalkJS Chatbox is the right half of the Inbox UI mode. It takes up less space and is designed to be embedded in your app near an order confirmation, a booking, or user profile.

Conversation list

The user’s conversations in one list. Put a Chatbox next to it to open the one they pick, or use the Inbox, which combines the two.

FAQs

No. It’s a warning, and the build still passes. With the TalkJS components imported at the top of a Vue component, our Vite build made one 1,200.68 kB JavaScript file (369.49 kB gzipped) and printed the warning. To put the chat components in a chunk of their own, import @talkjs/web-components with a dynamic import() where the chat mounts, as the Nuxt example above does.