Frameworks
The React kit (<HelpAI />, useHelpAI()), Next.js, Remix, Vue, Nuxt, Svelte, Astro, Angular, WordPress, Shopify, Webflow.
Last updated
Copy-paste recipes for every major frontend framework + CMS.
Contents#
- Next.js
- React (Vite / CRA)
- Vue 3
- Nuxt 3
- Svelte / SvelteKit
- Astro
- Remix
- SolidJS
- Angular
- WordPress
- Shopify
- Webflow
- Squarespace / Wix
Next.js#
The React kit, @helpai/elements/react, is server-render safe and already marked 'use client', so it drops straight
into a layout. Nothing touches the page until it runs in the browser.
App Router (13+):
// app/layout.tsx
import { HelpAI } from '@helpai/elements/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang='en'>
<body>
{children}
<HelpAI publicKey={process.env.NEXT_PUBLIC_HELPAI_KEY!} aiAgentDeploymentId={process.env.NEXT_PUBLIC_HELPAI_DEPLOYMENT_ID!} theme='host' />
</body>
</html>
);
}# .env.local
NEXT_PUBLIC_HELPAI_KEY=agent_pk_abc123...
NEXT_PUBLIC_HELPAI_DEPLOYMENT_ID=01900000-0000-7000-8000-000000000000A server component can only pass serializable props, so callbacks (onOpen, onMessage, …) go on a <HelpAI />
rendered from a 'use client' component of your own. useHelpAI() (below) works in any client component.
Pages Router: render <HelpAI /> in pages/_app.tsx the same way; no dynamic(..., { ssr: false }) needed.
React#
The React kit (React 18+) mounts the widget with <HelpAI /> and drives it from any component with useHelpAI():
import { HelpAI, useHelpAI } from '@helpai/elements/react';
export function Root() {
return (
<>
<main>{/* your app */}</main>
<HelpAI publicKey={import.meta.env.VITE_HELPAI_KEY} aiAgentDeploymentId={import.meta.env.VITE_HELPAI_DEPLOYMENT_ID} theme='host' />
</>
);
}
function PricingHelp() {
const { ask, open, state } = useHelpAI();
return (
<>
<button onClick={() => ask('Which plan fits a team of 10?')}>Ask about plans</button>
<button onClick={() => open({ to: 'help' })}>Help center {state.unread > 0 && `(${state.unread})`}</button>
</>
);
}<HelpAI {...options} />takes every widget option as a prop;aiAgentDeploymentIdis required. It renders nothing itself: the widget mounts once the page is in the browser, a prop change re-configures the live widget without a remount, and unmounting removes it. Callback props (onOpen,onMessage, …) can be new functions on every render; the widget always calls the latest one.useHelpAI(widgetId?)returns{ state, ask, open, close, reset, track }.stateis{ open, ready, unread, conversationId? }and re-renders the component when it changes; the server render sees the initial state. The commands are stable across renders and do nothing until the widget has mounted (state.ready).ask(text)fills the composer;ask(text, { send: true })sends it. Callreset()when your user logs out.- One import path. The kit carries the whole widget API (
getInstance,hook, the types), so import everything from@helpai/elements/react; the package's other entries are separate copies with their own widgets. - Two widgets on one page: give each
<HelpAI widgetId='support' … />and read it withuseHelpAI('support'). - Page tools:
useHelpAITool(tool)gives the agent a tool while the component is mounted, or pass<HelpAI tools={[...]} />(Page tools).executecan be a new function on every render; the agent's call runs the latest one, and only a changed definition registers or configures anything again. - Signed-in users: pass
userToken, a function that fetches the token your server signs (Verified users). Keep it stable for one user (a module function, oruseCallbackkeyed on your user id): a different function signs in afresh, and dropping it signs out.
The kit bundles the widget and imports only React from your app, so you don't need Preact installed.
React 19 — use the web component directly#
React 19+ supports custom elements natively:
// No JS bundle import needed — add a <script> tag or use the web-component sub-entry
import '@helpai/elements/web-component';
function Chat() {
return <web-ai-chat public-key={import.meta.env.VITE_HELPAI_KEY} ai-agent-deployment-id={import.meta.env.VITE_HELPAI_DEPLOYMENT_ID} theme='auto' />;
}TypeScript typings#
import type {} from '@helpai/elements/jsx/react'; once (in your entry file or a .d.ts) registers <web-ai-chat>
in React 19's JSX: every attribute the element reads autocompletes, ai-agent-deployment-id is required, and a ref
is the typed element with ask, open, state, ready and reset. Vue and Svelte have @helpai/elements/jsx/vue
and @helpai/elements/jsx/svelte (JSX typings).
import { useRef } from 'react';
import type {} from '@helpai/elements/jsx/react';
function Chat() {
const chat = useRef<HTMLElementTagNameMap['web-ai-chat']>(null);
return (
<>
<button onClick={() => chat.current?.ask('Which plan fits a team of 10?')}>Ask about plans</button>
<web-ai-chat ref={chat} public-key='agent_pk_...' ai-agent-deployment-id='...' theme='host' />
</>
);
}Vue 3#
Composition API:
<!-- components/Chat.vue -->
<script setup lang="ts">
import { onMounted, onBeforeUnmount } from 'vue';
import { init, type ChatWidgetInstance } from '@helpai/elements';
let instance: ChatWidgetInstance | null = null;
onMounted(() => {
instance = init({
publicKey: import.meta.env.VITE_HELPAI_KEY,
aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID,
});
});
onBeforeUnmount(() => {
instance?.destroy();
});
</script>
<template>
<!-- Nothing to render — the widget mounts its own DOM -->
</template><!-- App.vue -->
<script setup lang="ts">
import Chat from './components/Chat.vue';
</script>
<template>
<main>
<!-- your app -->
</main>
<Chat />
</template>Web component approach#
Vue 3 supports custom elements natively. Register the web-component sub-entry, then use the tag:
// main.ts
import { createApp } from 'vue';
import '@helpai/elements/web-component';
import App from './App.vue';
const app = createApp(App);
// Tell Vue to treat `web-ai-chat` as a custom element (not a Vue component)
app.config.compilerOptions.isCustomElement = (tag) => tag === 'web-ai-chat';
app.mount('#app');<template>
<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" theme="auto" />
</template>Nuxt 3#
// plugins/web-ai-chat.client.ts
import { init } from '@helpai/elements';
export default defineNuxtPlugin((nuxtApp) => {
const chat = init({
publicKey: useRuntimeConfig().public.helpaiKey,
aiAgentDeploymentId: useRuntimeConfig().public.helpaiDeploymentId,
});
nuxtApp.hook('app:beforeMount', () => {
// Widget already mounted by init(). This hook is just a reference.
});
// Clean up on app tear-down (rarely triggered in SPA navigation)
nuxtApp.hook('app:suspense:resolve', () => {
// No-op — widget stays across navigations
});
return {
provide: {
chat,
},
};
});// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
helpaiKey: process.env.NUXT_PUBLIC_HELPAI_KEY,
helpaiDeploymentId: process.env.NUXT_PUBLIC_HELPAI_DEPLOYMENT_ID,
},
},
});Note the .client.ts suffix — ensures the plugin only runs in the
browser.
Svelte / SvelteKit#
SvelteKit:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import { init, type ChatWidgetInstance } from '@helpai/elements';
import { PUBLIC_HELPAI_DEPLOYMENT_ID, PUBLIC_HELPAI_KEY } from '$env/static/public';
let instance: ChatWidgetInstance | null = null;
onMount(() => {
if (!browser) return;
instance = init({ publicKey: PUBLIC_HELPAI_KEY, aiAgentDeploymentId: PUBLIC_HELPAI_DEPLOYMENT_ID });
});
onDestroy(() => {
instance?.destroy();
});
</script>
<slot />Plain Svelte (no SvelteKit):
<!-- App.svelte -->
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { init, type ChatWidgetInstance } from '@helpai/elements';
let instance: ChatWidgetInstance | null = null;
onMount(() => {
instance = init({ publicKey: import.meta.env.VITE_HELPAI_KEY, aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID });
});
onDestroy(() => {
instance?.destroy();
});
</script>
<main>
<!-- your app -->
</main>Web component in Svelte#
Svelte handles custom elements natively:
<script lang="ts">
import '@helpai/elements/web-component';
</script>
<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" theme="auto" />Astro#
Astro is SSR-first; wrap the widget in a client directive:
---
// src/components/Chat.astro
const publicKey = import.meta.env.PUBLIC_HELPAI_KEY;
const aiAgentDeploymentId = import.meta.env.PUBLIC_HELPAI_DEPLOYMENT_ID;
---
<script define:vars={{ publicKey, aiAgentDeploymentId }}>
import('@helpai/elements').then(({ init }) => {
init({ publicKey, aiAgentDeploymentId });
});
</script>---
// src/layouts/Layout.astro
import Chat from '../components/Chat.astro';
---
<html>
<body>
<slot />
<Chat />
</body>
</html>Or use the web component (even simpler — no JS needed):
---
import '@helpai/elements/web-component';
---
<web-ai-chat public-key={Astro.env.PUBLIC_HELPAI_KEY} ai-agent-deployment-id={Astro.env.PUBLIC_HELPAI_DEPLOYMENT_ID} />Remix#
Remix (and React Router 7) render on the server, which the React kit handles: pass the keys from the loader.
// app/root.tsx
import { HelpAI } from '@helpai/elements/react';
import { Outlet, useLoaderData } from '@remix-run/react';
export async function loader() {
return {
helpaiKey: process.env.HELPAI_PUBLIC_KEY ?? '',
helpaiDeploymentId: process.env.HELPAI_DEPLOYMENT_ID ?? '',
};
}
export default function App() {
const { helpaiKey, helpaiDeploymentId } = useLoaderData<typeof loader>();
return (
<html>
<body>
<Outlet />
{helpaiKey && helpaiDeploymentId && <HelpAI publicKey={helpaiKey} aiAgentDeploymentId={helpaiDeploymentId} />}
</body>
</html>
);
}SolidJS#
import { init, type ChatWidgetInstance } from '@helpai/elements';
import { onMount, onCleanup } from 'solid-js';
export function Chat() {
let instance: ChatWidgetInstance | null = null;
onMount(() => {
instance = init({ publicKey: import.meta.env.VITE_HELPAI_KEY, aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID });
});
onCleanup(() => {
instance?.destroy();
});
return null;
}Solid also supports custom elements natively:
import '@helpai/elements/web-component';
export default function App() {
return <web-ai-chat public-key='agent_pk_...' ai-agent-deployment-id='01900000-0000-7000-8000-000000000000' />;
}Angular#
Angular needs CUSTOM_ELEMENTS_SCHEMA to accept the custom tag:
// src/app/app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@helpai/elements/web-component';
@NgModule({
// ...
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}<!-- app.component.html -->
<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" theme="auto"></web-ai-chat>For programmatic control:
// chat.service.ts
import { Injectable } from '@angular/core';
import { init, type ChatWidgetInstance } from '@helpai/elements';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class ChatService {
private instance: ChatWidgetInstance | null = null;
mount() {
if (!this.instance) {
this.instance = init({ publicKey: environment.helpaiKey, aiAgentDeploymentId: environment.helpaiDeploymentId });
}
}
destroy() {
this.instance?.destroy();
this.instance = null;
}
open() {
this.instance?.open();
}
}WordPress#
Add the snippet via the Theme Header/Footer plugin OR directly in
your theme's header.php:
<!-- Before </head> -->
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_abc123..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>Via a plugin#
Any "Insert Headers and Footers" plugin works:
- Install the plugin (e.g., "Insert Headers and Footers" by WPBeginner).
- Settings → Insert Headers and Footers.
- Paste the script tag into the "Scripts in Header" box.
- Save.
Via functions.php#
// Add to your theme's functions.php
function helpai_enqueue_chat() {
<script
src="https://cdn.help.ai/sdk/js/elements/latest/elements.js"
data-public-key="agent_pk_abc123..."
data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000"
defer
></script>
}
add_action('wp_head', 'helpai_enqueue_chat');Personalization for logged-in users#
function helpai_enqueue_chat_personalized() {
$user = wp_get_current_user();
$attrs = 'data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000"';
if ($user->ID) {
$attrs .= ' data-user-id="' . esc_attr($user->ID) . '"';
$attrs .= ' data-user-email="' . esc_attr($user->user_email) . '"';
$attrs .= ' data-user-name="' . esc_attr($user->display_name) . '"';
$attrs .= ' data-user-context=\'' . esc_attr(json_encode(array("plan" => "pro"))) . '\'';
}
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" echo $attrs; defer></script>
}
add_action('wp_head', 'helpai_enqueue_chat_personalized');Note:
data-user-*/data-page-*are host-asserted context for the AI, never an auth credential — the browser deployment is selected bydata-public-key+data-ai-agent-deployment-id. Explicit scalar attributes win over keys in thedata-user-context/data-page-contextJSON.
Shopify#
Shopify has a "custom pixels" and "additional scripts" workflow. Easiest path:
- In your admin: Online Store → Themes → Edit code.
- Open
theme.liquid. - Paste before
</head>:
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>With Shopify customer data#
{% if customer %}
<script
src="https://cdn.help.ai/sdk/js/elements/latest/elements.js"
data-public-key="agent_pk_..."
data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000"
data-user-id="{{ customer.id }}"
data-user-email="{{ customer.email }}"
data-user-name="{{ customer.first_name }} {{ customer.last_name }}"
data-page-area="orders"
defer
></script>
{% else %}
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
{% endif %}Webflow#
Webflow lets you add site-wide custom code:
- Site settings → Custom code → Head code.
- Paste:
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>- Save + publish.
Page-specific customization: open the page → Page settings →
Inside <head> tag and paste a page-specific snippet.
Squarespace / Wix#
Squarespace:
- Settings → Advanced → Code Injection.
- Paste the script tag into the Header section.
Wix:
- Settings → Custom Code.
- Add new snippet.
- Paste + choose "All pages" + "Head" placement.
Framework-agnostic troubleshooting#
The widget doesn't appear in SSR previews — that's expected. The widget is client-only. It appears after hydration / mount.
The widget flashes open then closes on SPA navigation — you're
probably calling destroy() then init() on every route change. Use
init() alone — it's idempotent. If there's an existing instance at
the same widgetId, it merges the new options in.
Hot module replacement breaks the widget — Next.js / Vite HMR
sometimes mounts duplicate instances. Always pair your useEffect /
onMounted with a cleanup that calls destroy().
CSP blocks the widget — the widget inlines styles inside a shadow
root (requires style-src 'unsafe-inline' or a nonce pattern). See
Security & privacy § CSP.
More: Troubleshooting.
Next#
- Recipes — programmatic control patterns.
- Events & hooks — analytics integration.
- Configuration — every option in detail.