Skip to content
GitHub
Get started →

Install on React (CRA, Vite)

React apps built with Create React App or Vite serve a static index.html at the root. The widget installs there — outside React’s tree, so there’s nothing to integrate.

Open index.html at the project root. Paste before </body>:

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script
src="https://spelo.ai/spelo.js"
data-site-id="YOUR_SITE_ID"
async
></script>
</body>
</html>

React Router route changes

React Router (v5 or v6) uses history.pushState for client-side navigation. The widget hooks into this automatically — it will re-read the DOM after each route change so the AI knows where the user is.

No extra code needed.

As a component (optional)

If you prefer to load the widget from a React component (e.g. to gate it behind a feature flag), use this hook:

src/hooks/useSpelo.ts
import { useEffect } from 'react'
export function useSpelo(siteId: string | undefined) {
useEffect(() => {
if (!siteId) return
if (document.querySelector(`script[data-site-id="${siteId}"]`)) return
const s = document.createElement('script')
s.src = 'https://spelo.ai/spelo.js'
s.setAttribute('data-site-id', siteId)
s.async = true
document.body.appendChild(s)
}, [siteId])
}
src/App.tsx
import { useSpelo } from './hooks/useSpelo'
export default function App() {
useSpelo(import.meta.env.VITE_SPELO_SITE_ID)
return <Routes />
}

Environment variables

ToolPrefixExample
ViteVITE_VITE_SPELO_SITE_ID
CRAREACT_APP_REACT_APP_SPELO_SITE_ID

StrictMode double-mount

React 18 in dev mode calls effects twice. The hook above is idempotent — it checks for an existing <script> tag before inserting. So you won’t end up with two widgets.

CSP

Same as raw HTML. If your React app sets a CSP:

script-src 'self' https://spelo.ai;
connect-src 'self' https://api.spelo.ai https://api.openai.com wss://*;
media-src 'self' blob:;

Build tooling notes

  • Vite — the widget is loaded over the network at runtime, so Vite doesn’t bundle it. Good.
  • CRA — same.
  • Webpack / custom builds — don’t try to import the widget. Always load via a <script> tag.
  • Micro-frontends — the widget uses Shadow DOM and a singleton pattern. If each micro-frontend tries to mount a separate widget, only the first one wins (it detects the existing instance and exits).

Verify

  1. npm run dev (Vite) or npm start (CRA)
  2. Hit http://localhost:5173 (Vite) or http://localhost:3000 (CRA)
  3. Orb appears at the bottom. Click → allow mic → speak.

Troubleshooting

  • Widget appears twice after a hot reload → only in dev; it self-dedupes on next full reload.
  • Widget gone after routing → React Router doesn’t cause this in practice. If you see it, open an issue with a repro.