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.
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.
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.0three(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';tsWithout 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>
);
}jsxProvider Props
| Prop | Type | Default | Description |
|---|---|---|---|
apiKey | string | -- | 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. |
onDeviceOnly | boolean | true | Use the standard local capture and measurement payload preparation path. Results are still returned through the Circadify API. |
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)}
/>
);
}jsxComponent Props
| Prop | Type | Default | Description |
|---|---|---|---|
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 (idle → readiness → scanning → uploading → processing → results | error). |
demographics | Demographics | -- | { age?, sex?, fitzpatrick? }. Optional, improves accuracy. |
autoStart | boolean | false | Start scanning immediately when the component mounts. |
showReadiness | boolean | true | Render the Readiness step (camera check + 4-channel quality pills) before scanning. |
showResults | boolean | true | Render the Results view after a successful scan. Set false if you want to render results in your own UI via onResult. |
showFeedback | boolean | true | Render the live quality meters during scanning. The status banner renders regardless of this setting. |
resultsActions | ReactNode | -- | 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. |
className | string | -- | CSS class applied to the root .cf-card element. |
style | React.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>
)}
</>
);
}jsxComposable 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.
| Component | Purpose |
|---|---|
CircadifyReadiness | Camera Check view: video preview, 4-channel quality pills (Face / Light / Still / Forward), optional gated Start button, in-frame loading/error overlay. |
CircadifyScanView | Active-scan view: video + R3F thermal-glow overlay + quality meters + carded progress bar + processing-state spinner. |
CircadifyResults | Post-scan metric stack with per-card clipboard copy, an actions slot, and an onRescan shortcut. |
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>
</>
);
}jsxPassing 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.
}jsxuseSession(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>
);
}jsxReturn values:
| Property | Type | Description |
|---|---|---|
session | Session | null | null 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. |
result | VitalSignsResult | null | The scan result once complete. |
error | CircadifyError | null | The error if the session failed. |
progress | number | Progress value from 0 to 1. |
isScanning | boolean | Whether 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 })-- encapsulatedgetUserMedialifecycle with permission and timeout handling. Returns{ stream, status, error, request, release }with errors mapped to typedCircadifyErrorcodes:request()acquires the camera and resolves with theMediaStream(ornullon failure, with the error surfaced viaerror), andrelease()stops all tracks and detaches the stream from the video element.summarizeQuality({ state, faceDetected })anddetailFor(channel, state, faceDetected)-- derive the four-channel UI summary (Face / Light / Still / Forward) from the SDK'sQualityStateevents. Use these to render your own quality pills or banners.formatVitals(result)andmetricToClipboard(metric)-- produce metric-card-ready payloads from aVitalSignsResult, 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>
);
}jsxCleanup
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();jsxNext Steps
- Custom UI -- Build a fully custom interface
- SDK Methods -- Full method reference