Circadify

React

Integrate the Circadify Web SDK into your React application with the built-in React bindings.

This guide walks you through integrating Circadify into a React application using the React bindings that ship with the Web SDK.

The React components and hooks ship inside @circadify/web-sdk at the @circadify/web-sdk/react subpath — there is no separate package to install. Follow the Web SDK installation guide to set up the Circadify SDK registry (sdk.circadify.com) with your Circadify SDK access token and run npm install @circadify/web-sdk; that single install gives you both the headless SDK and the React bindings.

Note

The deprecated @circadify/react@2.0.0 shim is no longer published. Import the React bindings from @circadify/web-sdk/react instead — they ship inside @circadify/web-sdk.

Note

The React bindings use these optional peer dependencies — install whichever your project doesn't already have. They are optional on the Web SDK, so headless (non-React) consumers are not required to install them:

  • react@>=18.0.0, react-dom@>=18.0.0
  • @react-three/fiber@^8.15.0, @react-three/drei@^9.88.0
  • three (any 0.151 -- 0.168)

Styles

The React bindings ship an opt-in stylesheet for the bundled views (Readiness, Scan, Results). Import it once -- typically alongside your global CSS or in your app entry point:

import '@circadify/web-sdk/react/styles.css';
ts

Without this import the components render unstyled. If you build a fully custom UI with the hooks only (never mounting <CircadifyScan> or the composable views), the stylesheet is optional.

Provider Setup

Wrap your application (or the relevant subtree) with the CircadifyProvider. This initializes the SDK and makes it available to all child components via React context.

import { CircadifyProvider } from '@circadify/web-sdk/react';
 
function App() {
  return (
    <CircadifyProvider apiKey="ck_live_your_key_here">
      <YourApp />
    </CircadifyProvider>
  );
}
jsx

Provider Props

PropTypeDefaultDescription
apiKeystring--Your Circadify API key (ck_test_* or ck_live_*). Required.
environment'production' | 'sandbox''production'API host selector: 'sandbox' routes to api.sandbox.circadify.com. This only picks the host — behavior is driven by the key prefix (ck_test_* keys always return simulated vitals; ck_live_* keys run real measurements) on either host.
onProgress(progress: ProgressEvent) => void--Global callback for scan progress updates.
onQualityWarning(warning: QualityWarning) => void--Called when lighting, movement, or face positioning issues are detected.
onDeviceOnlybooleantrueUse the standard local capture and measurement payload preparation path. Results are still returned through the Circadify API.
Tip

Place the provider as high in the tree as needed, but not higher. If only one route uses scanning, wrap just that route to avoid initializing the SDK unnecessarily.

Using the Component

The CircadifyScan component renders a complete scanning interface with a camera feed, face guide overlay, progress indicator, and quality warnings. It handles the full session lifecycle automatically.

import { CircadifyScan } from '@circadify/web-sdk/react';
 
function ScanPage() {
  const handleResult = (result) => {
    console.log('Heart Rate:', result.heartRate);
    console.log('Confidence:', result.confidence);
  };
 
  return (
    <CircadifyScan
      type="standard"
      onResult={handleResult}
      onError={(error) => console.error(error)}
    />
  );
}
jsx

Component Props

PropTypeDefaultDescription
type'standard''standard'The scan session type.
onResult(result: VitalSignsResult) => void--Called when a scan completes successfully.
onError(error: CircadifyError) => void--Called when a scan fails.
onProgress(progress: ProgressEvent) => void--Called during scan with progress updates.
onPhaseChange(phase: ScanPhase) => void--Fires on every state-machine transition (idlereadinessscanninguploadingprocessingresults | error).
demographicsDemographics--{ age?, sex?, fitzpatrick? }. Optional, improves accuracy.
autoStartbooleanfalseStart scanning immediately when the component mounts.
showReadinessbooleantrueRender the Readiness step (camera check + 4-channel quality pills) before scanning.
showResultsbooleantrueRender the Results view after a successful scan. Set false if you want to render results in your own UI via onResult.
showFeedbackbooleantrueRender the live quality meters during scanning. The status banner renders regardless of this setting.
resultsActionsReactNode--Slot rendered under the metric stack on the Results view (share/save buttons, external CTAs).
onRescan() => void--When supplied, a default outlined "Scan Again" button is appended to the Results actions slot and the orchestrator resets to readiness on click.
classNamestring--CSS class applied to the root .cf-card element.
styleReact.CSSProperties--Inline styles for the root element.

Handling Results

The onResult callback receives a VitalSignsResult object:

function ScanPage() {
  const [vitals, setVitals] = useState(null);
 
  return (
    <>
      <CircadifyScan
        type="standard"
        demographics={{ age: 35, sex: 'M' }}
        onResult={(result) => setVitals(result)}
        onError={(error) => alert(error.message)}
      />
      {vitals && (
        <div>
          <p>Heart Rate: {vitals.heartRate} BPM</p>
          <p>SpO2: {vitals.spo2}%</p>
          <p>Confidence: {(vitals.confidence * 100).toFixed(0)}%</p>
        </div>
      )}
    </>
  );
}
jsx

Composable Subcomponents

When the all-in-one <CircadifyScan> doesn't fit -- for example, you want a custom view between Readiness and Results, or you want to render Results inside a modal -- import the underlying views and pair them with useSession() (or your own state).

Each subcomponent is presentational and props-driven; none of them own SDK lifecycle.

ComponentPurpose
CircadifyReadinessCamera Check view: video preview, 4-channel quality pills (Face / Light / Still / Forward), optional gated Start button, in-frame loading/error overlay.
CircadifyScanViewActive-scan view: video + R3F thermal-glow overlay + quality meters + carded progress bar + processing-state spinner.
CircadifyResultsPost-scan metric stack with per-card clipboard copy, an actions slot, and an onRescan shortcut.
Want the face / heat-glow overlay? Use CircadifyScanView — don't rebuild it

CircadifyScanView already renders the thermal/heat-glow face overlay for you. Mount it instead of hand-rolling a three.js / @react-three/fiber overlay from onLandmarks.

If you genuinely need a custom overlay, prefer a 2D <canvas> in immediate mode (see the Custom UI guide) — it has no retained geometry to freeze. Only reach for r3f as a last resort, and if you do, never create BufferGeometry/BufferAttributes in useMemo (React discards it under load and the overlay renders once then freezes) — build them once via useState lazy-init, set frameloop="always", frustumCulled={false}, an orthographic camera at zoom={1}, a z-indexed overlay layer, and clear the overlay on empty-landmark frames. Landmarks are normalized [0,1] and not mirrored. Full checklist (incl. the @react-three/fiber v9 gotcha): Custom UI → Advanced: a 3D (r3f) overlay.

import {
  CircadifyProvider,
  CircadifyReadiness,
  CircadifyResults,
  useSession,
  useCamera,
} from '@circadify/web-sdk/react';
import { useRef } from 'react';
import '@circadify/web-sdk/react/styles.css';
 
function CustomFlow() {
  const session = useSession();
  const videoRef = useRef(null);
  const camera = useCamera({ videoRef });
 
  if (session.result) {
    return <CircadifyResults result={session.result} onRescan={() => session.start()} />;
  }
  return (
    <>
      {/* Camera check: live preview + permission/error overlay. landmarks and
          quality stay null here — the SDK emits them only while measureVitals()
          is running, so there is nothing to pass before the scan starts. */}
      <CircadifyReadiness
        videoRef={videoRef}
        landmarks={null}
        quality={null}
        cameraStatus={camera.status}
        cameraError={camera.error}
        onCameraRetry={() => camera.request()}
      />
      {/* Render your own Start button — see the caution below for why the
          built-in one (via onStart) cannot work in this setup. */}
      <button
        type="button"
        disabled={camera.status !== 'ready' || session.isScanning}
        onClick={() => {
          camera.release(); // hand the camera over to the SDK
          session.start({ videoElement: videoRef.current });
        }}
      >
        {session.isScanning ? 'Scanning…' : 'Start Scan'}
      </button>
    </>
  );
}
jsx
Don't use CircadifyReadiness's built-in Start button in a composable flow

Passing onStart to <CircadifyReadiness> renders its built-in Start button, but that button only enables when the quality prop reports every channel good. The SDK emits QualityState only while measureVitals() is running — before the scan starts there is no quality state to pass, so with quality={null} the built-in button stays disabled forever. Render your own Start button instead (as above) and let the SDK enforce readiness itself: after start(), measureVitals() runs its internal readiness gate (face + lighting + stillness + pose, 30-second deadline) before capture begins. If you want live quality pills during that gate, use the all-in-one <CircadifyScan> (it starts the measurement immediately and feeds the SDK's per-frame quality state into its readiness view), or construct CircadifySDK directly with an onQualityState callback and manage that state yourself — the provider does not expose a public per-frame QualityState subscription.

Using Hooks

For more control over the scanning lifecycle, use the SDK hooks directly. This is useful when you need a custom UI or want to integrate scanning into an existing component.

useCircadify()

Returns the underlying CircadifySDK instance from the nearest CircadifyProvider. Use this for direct SDK access.

import { useCircadify } from '@circadify/web-sdk/react';
 
function DebugPanel() {
  const sdk = useCircadify();
  // Access sdk.measureVitals(), sdk.destroy(), sdk.cancel(), sdk.getDeviceCapabilities(), etc.
}
jsx

useSession(options)

Manages a scan session with start/stop controls and reactive state. Returns the session state, control functions, and result data.

import { useCircadify, useSession } from '@circadify/web-sdk/react';
 
function CustomScan() {
  const client = useCircadify();
  const { session, start, stop, result, error, progress, isScanning } = useSession({
    type: 'standard',
  });
 
  return (
    <div>
      <button onClick={() => start()} disabled={isScanning}>Start Scan</button>
      <button onClick={stop} disabled={!isScanning}>Stop</button>
 
      {isScanning && <p>Progress: {Math.round(progress * 100)}%</p>}
      {error && <p>Error: {error.message}</p>}
      {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
    </div>
  );
}
jsx

Return values:

PropertyTypeDescription
sessionSession | nullnull for the entire duration of the scan — it is only populated after measureVitals() resolves, with a synthetic record ({ sessionId, status: 'completed', expiresAt: 0 }). Don't read it mid-scan; use isScanning/progress for live state and take sessionId from the result.
start(opts?: UseSessionStartOptions) => Promise<void>Start a new scan session. Optional opts: { signal?, demographics?, videoElement? }. Pass a videoElement to drive your own <video> instead of letting the SDK create one.
stop() => Promise<void>Stop the active session.
resultVitalSignsResult | nullThe scan result once complete.
errorCircadifyError | nullThe error if the session failed.
progressnumberProgress value from 0 to 1.
isScanningbooleanWhether a scan is currently in progress.

Utility hooks and helpers

When you're driving the camera and feedback UI yourself, three additional helpers from @circadify/web-sdk/react make custom flows easier:

  • useCamera({ videoRef, timeoutMs }) -- encapsulated getUserMedia lifecycle with permission and timeout handling. Returns { stream, status, error, request, release } with errors mapped to typed CircadifyError codes: request() acquires the camera and resolves with the MediaStream (or null on failure, with the error surfaced via error), and release() stops all tracks and detaches the stream from the video element.
  • summarizeQuality({ state, faceDetected }) and detailFor(channel, state, faceDetected) -- derive the four-channel UI summary (Face / Light / Still / Forward) from the SDK's QualityState events. Use these to render your own quality pills or banners.
  • formatVitals(result) and metricToClipboard(metric) -- produce metric-card-ready payloads from a VitalSignsResult, including pretty units and clipboard strings.

Cancellation with AbortController

You can pass an AbortController signal through the hook to allow cancellation:

function CancellableScan() {
  const controllerRef = useRef(null);
  const { start, stop, result } = useSession({ type: 'standard' });
 
  const handleStart = () => {
    controllerRef.current = new AbortController();
    start({ signal: controllerRef.current.signal });
  };
 
  const handleCancel = () => {
    controllerRef.current?.abort();
  };
 
  return (
    <div>
      <button onClick={handleStart}>Start</button>
      <button onClick={handleCancel}>Cancel</button>
    </div>
  );
}
jsx

Cleanup

The CircadifyProvider automatically calls sdk.destroy() when it unmounts, releasing camera streams, runtime resources, and event listeners. No manual cleanup is required in most cases.

If you need to manually destroy the SDK instance (for example, on logout), use the useCircadify hook:

const sdk = useCircadify();
sdk.destroy();
jsx

Next Steps