# Frameworks

> The React kit (<HelpAI />, useHelpAI()), Next.js, Remix, Vue, Nuxt, Svelte, Astro, Angular, WordPress, Shopify, Webflow.

Copy-paste recipes for every major frontend framework + CMS.

---

## Contents

- [Next.js](#nextjs)
- [React (Vite / CRA)](#react)
- [Vue 3](#vue-3)
- [Nuxt 3](#nuxt-3)
- [Svelte / SvelteKit](#svelte--sveltekit)
- [Astro](#astro)
- [Remix](#remix)
- [SolidJS](#solidjs)
- [Angular](#angular)
- [WordPress](#wordpress)
- [Shopify](#shopify)
- [Webflow](#webflow)
- [Squarespace / Wix](#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+):

```tsx
// 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>
  );
}
```

```bash
# .env.local
NEXT_PUBLIC_HELPAI_KEY=agent_pk_abc123...
NEXT_PUBLIC_HELPAI_DEPLOYMENT_ID=01900000-0000-7000-8000-000000000000
```

A 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()`:

```tsx
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](/sdk/configuration) as a prop; `aiAgentDeploymentId` is
  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 }`. `state` is
  [`{ open, ready, unread, conversationId? }`](/sdk/api-reference#state-and-ready) 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. Call `reset()`
  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 with `useHelpAI('support')`.
- **Page tools:** `useHelpAITool(tool)` gives the agent a tool while the component is mounted, or pass
  `<HelpAI tools={[...]} />` ([Page tools](/sdk/page-tools)). `execute` can 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](/sdk/security-privacy#verified-users-usertoken)). Keep it stable for one user (a module function, or
  `useCallback` keyed 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:

```tsx
// 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](/sdk/api-reference#jsx-typings)).

```tsx
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:

```vue
<!-- 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>
```

```vue
<!-- 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:

```ts
// 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');
```

```vue
<template>
  <web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" theme="auto" />
</template>
```

---

## Nuxt 3

```ts
// 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,
    },
  };
});
```

```ts
// 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**:

```svelte
<!-- 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):

```svelte
<!-- 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:

```svelte
<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:

```astro
---
// 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>
```

```astro
---
// 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):

```astro
---
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](#react) handles: pass the keys from the loader.

```tsx
// 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

```tsx
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:

```tsx
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:

```ts
// 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 {}
```

```html
<!-- 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:

```ts
// 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`:

```html
<!-- 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:

1. Install the plugin (e.g., "Insert Headers and Footers" by WPBeginner).
2. Settings → Insert Headers and Footers.
3. Paste the script tag into the "Scripts in Header" box.
4. Save.

### Via functions.php

```php
<?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>
  <?php
}
add_action('wp_head', 'helpai_enqueue_chat');
```

### Personalization for logged-in users

```php
<?php
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" <?php echo $attrs; ?> defer></script>
  <?php
}
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 by
> `data-public-key` + `data-ai-agent-deployment-id`. Explicit
> scalar attributes win over keys in the `data-user-context` /
> `data-page-context` JSON.

---

## Shopify

Shopify has a "custom pixels" and "additional scripts" workflow.
Easiest path:

1. In your admin: **Online Store → Themes → Edit code**.
2. Open `theme.liquid`.
3. Paste before `</head>`:

```html
<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

```html
{% 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:

1. Site settings → **Custom code → Head code**.
2. Paste:

```html
<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>
```

3. Save + publish.

Page-specific customization: open the page → **Page settings →
Inside `<head>` tag** and paste a page-specific snippet.

---

## Squarespace / Wix

**Squarespace:**

1. Settings → **Advanced → Code Injection**.
2. Paste the script tag into the **Header** section.

**Wix:**

1. Settings → **Custom Code**.
2. Add new snippet.
3. 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](/sdk/security-privacy#content-security-policy).

More: [Troubleshooting](/sdk/troubleshooting).

---

## Next

- [Recipes](/sdk/recipes) — programmatic control patterns.
- [Events & hooks](/sdk/events-and-hooks) — analytics integration.
- [Configuration](/sdk/configuration) — every option in detail.
