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>:
<!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>Open public/index.html. Paste before </body>:
<body> <div id="root"></div>
<script src="https://spelo.ai/spelo.js" data-site-id="YOUR_SITE_ID" async ></script></body>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:
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])}import { useSpelo } from './hooks/useSpelo'
export default function App() { useSpelo(import.meta.env.VITE_SPELO_SITE_ID) return <Routes />}Environment variables
| Tool | Prefix | Example |
|---|---|---|
| Vite | VITE_ | VITE_SPELO_SITE_ID |
| CRA | REACT_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
importthe 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
npm run dev(Vite) ornpm start(CRA)- Hit
http://localhost:5173(Vite) orhttp://localhost:3000(CRA) - 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.