Install PodSaid in a React app
For Next.js, see the dedicated Next.js guide — the SSR / hydration story is different. This page is for client-rendered React apps: Vite, Create React App, or any plain-React SPA. If you’re using Remix or React Router v7 in SSR mode, follow the Next.js guide’s SSR advice.
The widget is not a React component — it’s a custom element registered by an IIFE script. React talks to it via ordinary DOM attributes.
Time: 5 minutes including TypeScript setup.
What you need before you start:
- A PodSaid account with a feed set up (see Quickstart).
- The embed snippet’s two values: your
feed-idandpk_live_key. Grab them from Console → Settings → Embed Code. - The domain(s) your app is served from added to your Allowed
Origins. Include
http://localhost:5173(Vite) orhttp://localhost:3000(CRA) for local dev — you can leave localhost in the allowlist indefinitely. See Allowed origins.
Step 1 — Load the widget script
Add the loader once in your app’s static HTML shell — not inside a React component.
Vite — edit index.html at the project root:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My app</title>
</head>
<body>
<div id="root"></div>
<script src="https://widget.podsaid.com/v1/widgets.iife.js" defer></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html> Create React App — edit public/index.html:
<body>
<div id="root"></div>
<script src="https://widget.podsaid.com/v1/widgets.iife.js" defer></script>
</body> Why not import it in a component? The widget script uses
side effects to register the <ask-pod-widget> custom element with
the browser at load time. Importing it from inside a component
couples that side effect to the component’s lifecycle, which fights
React’s reconciler — you can end up with double-registration
warnings, or with the element registering after the JSX tries to
use it. Load it once, statically, in the HTML shell.
Step 2 — Use <ask-pod-widget> in JSX
Drop the element in your App.tsx (or wherever you want the widget
mounted). Attributes are kebab-case:
export default function App() {
return (
<>
{/* your app UI */}
<ask-pod-widget
feed-id="yourfeedslug"
api-key="pk_live_xxxxxxxxxxxxxxxx"
/>
</>
);
} The widget mounts a floating chat launcher (default: bottom-right). Its UI lives inside a Shadow DOM, so your app’s CSS can’t affect it and its CSS can’t leak into your app.
Why kebab-case attributes and not camelCase? React’s props →
attributes conversion only kebab-cases well-known HTML attributes.
For unknown custom-element attributes (any name with a hyphen or
that React doesn’t recognize), it passes props through as-is. If
you write feedId="...", React sets a feedId attribute on the
element and the widget doesn’t see it. Write feed-id="..." and
it works. React 19 relaxes this — camelCase is auto-converted for
custom elements — but kebab-case works everywhere including older
React versions.
Step 3 — TypeScript setup
Without a type declaration, TSX will complain that ask-pod-widget isn’t a valid JSX element. Add this to any .d.ts file in your
project (src/global.d.ts is a common name):
// src/global.d.ts
import 'react';
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'ask-pod-widget': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'feed-id': string;
'api-key': string;
},
HTMLElement
>;
}
}
} If you’re on an older setup that puts JSX types in the global JSX namespace (Create React App, older Vite templates), use this
form instead:
// src/global.d.ts
declare namespace JSX {
interface IntrinsicElements {
'ask-pod-widget': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
'feed-id': string;
'api-key': string;
},
HTMLElement
>;
}
} TypeScript picks it up via your tsconfig.json include (already
includes .d.ts files by default).
Where to mount the element
- In
App.tsx(top-level) — the floating launcher shows on every route. Recommended for most sites. - Inside a specific route component — restricts the widget to that route. Useful if you only want the widget on, say, a Sermons or Episodes page.
Router integration: none required. The widget mounts once and
manages its own UI outside the React tree. Route changes (React
Router, TanStack Router, whatever) don’t need to unmount or
re-mount the widget. Don’t call useEffect cleanup on it.
Common React gotchas
1. Attribute name mismatch
Symptom: Widget appears but is stuck on the “still preparing”
placeholder — or DevTools shows [podsaid-widget] Auth failed (400): missing_feed_id.
Cause: You wrote feedId="..." or apiKey="..." (camelCase)
in JSX. React set them as feedId / apiKey attributes; the
widget looks for feed-id / api-key.
Fix: Change to kebab-case. Or upgrade to React 19, which auto-converts camelCase attribute names for custom elements.
2. Widget script imported inside a component
Symptom: Console warnings about customElements.define() being
called more than once, or the widget flickers on route change.
Cause: You added import 'https://widget.podsaid.com/...' or a <script> inside a React component, so React re-runs it on every
mount. Custom-element registration is a one-time-per-name
operation; running it twice throws or warns.
Fix: Move the <script src="..."> tag out of any React
component and into index.html (see Step 1). Load it once.
3. Server-rendering hydration mismatch (Next.js / Remix)
Symptom: Hydration warning like “did not match server-rendered HTML”.
Cause: You’re on Next.js or Remix in SSR mode, and this guide
doesn’t fit — the widget can’t render on the server (custom
elements need document), and React’s hydrator flags the mismatch.
Fix: Follow the Next.js guide instead. It
covers dynamic() with ssr: false, and the equivalent for
Remix / React Router v7 SSR mode.
4. Local dev requests fail with a CORS error
Symptom: Widget mounts but questions fail with [podsaid-widget] Auth failed (403): origin_denied on http://localhost:5173 or http://localhost:3000.
Cause: Localhost is a distinct origin. It’s not in your Allowed Origins list.
Fix: In Console → Settings → Allowed Origins, add http://localhost:5173 (or your dev port). Leave it in the
allowlist permanently — costs nothing.
Verifying it works
Run your dev server (
npm run devfor Vite,npm startfor CRA) and open the app in a browser.Look for the floating launcher (default: bottom-right).
Click it. If a chat box slides in, you’re done.
Open DevTools console (⌥⌘I on Mac, F12 on Windows). Look for:
[podsaid-widget] v1.8.2 mounted (feed=yourfeedslug, api=https://api.podsaid.com)
If the widget appears but questions fail, jump to Widget errors — what each one means.
When the widget shows “still preparing your content”
If ingest hasn’t finished for your feed yet, the widget renders a friendly placeholder instead of an error. Your paste is correct — you just don’t have content to answer questions against yet. Watch progress under Console → Feed → Ingest status, or wait for the “your widget is live” email.
See How ingest works (and why it takes hours).
Match your brand
The widget renders inside a Shadow DOM, so your site’s CSS can’t reach the widget and the widget’s CSS can’t leak into your site — nothing in your theme will accidentally break the widget’s look. To make it match your church or podcast brand, the widget exposes a set of CSS custom properties and Shadow Parts you can target from your own stylesheet.
See Customization (CSS) for the full list, with copy-paste examples.
Still stuck?
- Errors index: Widget errors — what each one means
- Book a 15-minute call with Mark: /docs/support
- Email:
[email protected]— include the live URL, a screenshot of the browser console, your React version, and whether you’re on Vite, CRA, or something else