Integration guides
Headless hooks
Build a fully custom UI on @dialogueai/react hooks — the same contract the styled layer is built on. You own every pixel; the SDK owns the journey.
The shape of a headless UI
A headless UI is a switch on useInterview().phase, with each phase rendered by your own markup driven by the relevant hook. No react-ui import is involved.
A complete example
CustomInterview.tsxtsx
'use client';
import {
DialogueProvider, useInterview, useConsent,
useParticipantProfile, useScreener, UnloadGuard,
} from '@dialogueai/react';
function CustomInterview({ studyId }: { studyId: string }) {
const { phase, study, start, end } = useInterview(studyId);
const consent = useConsent();
return (
<main>
<UnloadGuard />
{phase === 'consent' && (
<>
<h2>{study?.name ?? 'Interview'}</h2>
<button onClick={() => consent.accept()}>Accept</button>
<button onClick={() => consent.decline()}>Decline</button>
</>
)}
{phase === 'profile' && <ProfileStep />}
{phase === 'screener' && <ScreenerStep />}
{phase === 'deviceCheck' && <button onClick={() => start()}>Start interview</button>}
{phase === 'connecting' && <p>Connecting…</p>}
{phase === 'live' && <button onClick={() => end()}>End interview</button>}
{phase === 'completed' && <p>Thanks — interview complete.</p>}
{phase === 'screenedOut' && <p>Not eligible for this study.</p>}
</main>
);
}
export function Headless({ studyId }: { studyId: string }) {
return (
<DialogueProvider bootstrapTokenProvider={getToken}>
<CustomInterview studyId={studyId} />
</DialogueProvider>
);
}The profile and screener steps are their own hooks:
tsx
function ProfileStep() {
const { update, next, isSaving } = useParticipantProfile();
// update({ firstName, lastInitial }) PATCHes immediately; next() advances.
}
function ScreenerStep() {
const { question, answer, setAnswer, canContinue, submit } = useScreener();
// submit() advances through questions, then leaves the screener phase.
}Unload protection
Drop <UnloadGuard /> anywhere inside the provider. It warns the participant before they close the tab mid-interview and flushes a reliable end-of-session signal. The styled layer includes it automatically.
Mix and match: use the styled
<Interview> for most phases but reach for hooks like useMediaControls() or useAgent() to build custom in-call controls on top.When to go headless
- You need the interview to match a bespoke design system pixel-for-pixel.
- You want custom copy, layout, or step ordering within what the journey allows.
- You’re embedding inside an existing flow with its own chrome.
If you just want it branded and working, prefer the styled drop-in.