React and Next.js Widget Embeds
The widget is a framework-free custom element-style controller, but it drops into component trees cleanly: init in an effect, destroy on unmount. This recipe covers the two embeds that come up most - Vite/React and Next.js App Router - plus the proxy pattern for hiding your backend URL.
React (Vite)
npm create vite@latest my-trap -- --template react
cd my-trap && npm install tokentrap-ai
Replace src/App.jsx:
import { useEffect, useRef } from "react";
import { TokenTrap } from "tokentrap-ai";
export default function App() {
const ref = useRef(null);
useEffect(() => {
const trap = TokenTrap.init({
container: ref.current,
persona: "Internal AI Assistant",
trapStrength: "aggressive",
onInteraction(log) {
console.log("[TokenTrap]", log);
},
});
return () => trap.destroy();
}, []);
return <div ref={ref} style={{ width: "min(680px,100%)", height: 560 }} />;
}
Notes from the field:
TokenTrap.initaccepts either an element (ref.current) or a CSS selector string. The element ref avoids timing issues with selectors in Strict Mode double-renders.- The cleanup return matters:
destroy()removes the shadow-DOM UI and event listeners. Without it, hot reload and route changes stack duplicate widgets. - If Strict Mode gives you double-init flicker, the destroy/re-init pair handles it - don't disable Strict Mode for this.
Next.js (App Router)
npx create-next-app@latest my-trap --app --ts
cd my-trap && npm install tokentrap-ai
Create app/trap/page.tsx:
"use client";
import { useEffect, useRef } from "react";
import { TokenTrap } from "tokentrap-ai";
export default function TrapPage() {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const trap = TokenTrap.init({
container: ref.current,
persona: "Internal AI Assistant",
theme: "dark",
});
return () => trap.destroy();
}, []);
return <div ref={ref} style={{ width: "min(680px,100%)", height: 560 }} />;
}
npm run dev, open /trap, done. The "use client" directive is required - the widget touches the DOM, so it never renders server-side.
Proxying the backend
Don't want https://tokentrap-worker.you.workers.dev visible in client code? Put a route handler in front of it.
Next.js App Router example at app/api/trap-chat/route.ts:
const BACKEND = process.env.TRAP_BACKEND_URL ?? "http://127.0.0.1:8787";
export async function POST(request: Request) {
const body = await request.json();
const upstream = await fetch(`${BACKEND}/api/chat`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
return new Response(upstream.body, {
status: upstream.status,
headers: { "content-type": "application/json" },
});
}
Then point the widget at your own origin:
TokenTrap.init({
container: ref.current,
apiEndpoint: "/api/trap-chat",
});
The widget appends /api/chat to whatever base you give it, so the proxy receives /api/trap-chat/api/chat requests and forwards them. Forward the x-tokentrap-canary-echo response header too if your alerting reads it client-side.
Headless inside a component tree
Sometimes you want trap behavior with none of the bundled UI - say, rendering replies into your own design system. Skip the container entirely:
const trapRef = useRef(null);
useEffect(() => {
trapRef.current = TokenTrap.init({ showUI: false });
}, []);
async function handleSubmit(text) {
const res = await trapRef.current.send(text);
// render res.reply however you like; store res.meta for telemetry
}
send() resolves with {reply, meta} or throws (non-empty message required; backend errors surface as rejections plus kind: "error" interaction logs). Backend calls time out after 20 seconds.
Theming
The bundled chat UI ships three themes via theme: "dark" | "light" | "auto" - dark-first palette (#0b0f14 ground, green/cyan accents), light variant for bright sites, auto follows prefers-color-scheme. The UI renders inside shadow DOM, so page styles neither leak in nor out; you can drop the widget next to any design system without collisions.