For developers

Frameworks

Where the two lines go in Next.js, Nuxt, React, Vue, Angular, WordPress, Tilda and Bitrix.

On this page

Every stack uses the same two lines (Install on a site). Add the script once and the element once, in the layout all pages share. Replace your-project with the project's id.

Plain HTML

Add the lines before </body> on every page, or in the template that builds them:

HTML
  <script type="module" src="https://api.tolkly.io/widget.js"></script>
  <tolkly-chat endpoint="https://api.tolkly.io" project="your-project"></tolkly-chat>
</body>

Other server templates work the same way: Laravel's Blade layout, Django's base.html, Rails' application.html.erb. Put the lines before </body> of the base layout.

Next.js (App Router)

The root layout stays mounted when the visitor navigates. Put the chat there:

TSX
// app/layout.tsx
import Script from 'next/script'
import type { ReactNode } from 'react'

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <tolkly-chat endpoint="https://api.tolkly.io" project="your-project"></tolkly-chat>
        <Script type="module" src="https://api.tolkly.io/widget.js" strategy="afterInteractive" />
      </body>
    </html>
  )
}
  • next/script passes type="module" on to the script it adds.
  • With afterInteractive, the widget runs after React hydrates the page.
  • React 19 renders the custom element with its attributes as written.
  • Add the type declaration below, or TypeScript rejects <tolkly-chat>.

Next.js (Pages Router)

_app wraps every page and stays mounted on navigation:

TSX
// pages/_app.tsx
import type { AppProps } from 'next/app'
import Script from 'next/script'

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <tolkly-chat endpoint="https://api.tolkly.io" project="your-project"></tolkly-chat>
      <Script type="module" src="https://api.tolkly.io/widget.js" strategy="afterInteractive" />
    </>
  )
}

TypeScript and JSX

React 19 types read intrinsic elements from React.JSX. Declare the tag once in any .d.ts file that your tsconfig.json includes, for example tolkly.d.ts:

TypeScript
import type { DetailedHTMLProps, HTMLAttributes } from 'react'

type TolklyChatProps = DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement> & {
  endpoint?: string
  project?: string
  name?: string
  greeting?: string
  handoff?: string
  teaser?: string
  position?: 'left' | 'right'
  theme?: 'site' | 'system' | 'light' | 'dark'
  'send-icon'?: 'arrow' | 'plane' | 'forward'
  collapse?: string
  'data-identity'?: string
}

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'tolkly-chat': TolklyChatProps
    }
  }
}

The import makes the file a module. Thanks to it, declare module 'react' adds to React's types instead of replacing them. HTMLAttributes already covers lang, nonce, style and className.

Do not pass open and inline in JSX. If the element has a DOM property with the same name, React 19 sets the prop as that property. <tolkly-chat> has an open() method and a read-only inline property. Write these attributes in index.html instead, or call open({ focus: false }) on the element from an effect.

Nuxt 3

Load the script from nuxt.config.ts and tell Vue the tag is a custom element:

TypeScript
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      script: [{ src: 'https://api.tolkly.io/widget.js', type: 'module' }],
    },
  },
  vue: {
    compilerOptions: {
      isCustomElement: (tag) => tag.startsWith('tolkly-'),
    },
  },
})

Then put the element in app.vue, outside <NuxtPage>:

Vue
<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
  <tolkly-chat endpoint="https://api.tolkly.io" project="your-project"></tolkly-chat>
</template>

React with Vite

Put both lines in index.html, before </body>, outside the #root element that React renders into. You do not need to change the React code. If you prefer to render <tolkly-chat> from JSX, render it once in the root component and add the type declaration.

Vue 3 with Vite

Put both lines in index.html, before </body>, outside the #app element. If you prefer to use the tag in a Vue template, tell the compiler it is a custom element:

TypeScript
// vite.config.ts
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: { isCustomElement: (tag) => tag.startsWith('tolkly-') },
      },
    }),
  ],
})

Angular

Put both lines in src/index.html, before </body>, outside the app's root element. Do not add the script to scripts in angular.json. That bundles it as a classic script, and it will not run.

If you prefer to use the tag in a component template, allow custom elements there:

TypeScript
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppComponent {}

WordPress

Add the lines just before </body> in your theme's footer.php. Use a child theme, so a theme update does not remove them. Or add them from the theme's functions.php:

PHP
add_action('wp_footer', function () {
    echo '<script type="module" src="https://api.tolkly.io/widget.js"></script>';
    echo '<tolkly-chat endpoint="https://api.tolkly.io" project="your-project"></tolkly-chat>';
});

No access to theme files? Any plugin that adds custom code before </body> on every page will do. Then clear the page cache, if the site has one.

Tilda

Add both lines to an HTML code block (T123). Put the block in a footer that every page uses, or on each page where the chat should show. Then publish the pages again.

1C-Bitrix

Add the lines just before </body> in the site template's footer.php. It is at /local/templates/<template>/footer.php or /bitrix/templates/<template>/footer.php. If the template is cached, clear the site cache afterwards.