REST API ↗
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.