Skip to content

Custom UI

Use headless mode combined with the SDK’s event system to build a scanning interface that perfectly matches your application’s design language.

When building a custom UI, you handle all visual rendering while the SDK manages capture, processing, and result delivery. The separation of concerns looks like this:

+---------------------+ Events +---------------------+
| Your Code | <------------------ | Circadify SDK |
| | | |
| - Render UI | session:progress | - Camera access |
| - Progress ring | session:complete | - Face detection |
| - Quality messages | session:error | - Vital signs |
| - Result display | quality warnings | processing |
| - Error states | | - WASM runtime |
+---------------------+ +---------------------+
| |
| User clicks "Start" |
+------ start() --------------------------->|
| User clicks "Cancel" |
+------ abort() --------------------------->|
  1. Your code — Renders the UI, handles user interaction, and displays all visual feedback
  2. Circadify SDK — Manages camera access, face detection, on-device processing, and result delivery
  3. Events — The SDK communicates state changes (progress, quality warnings, errors, results) via its event system

Initialize the SDK without a container option. This puts the SDK in headless mode where it performs processing but does not render any DOM elements:

import { CircadifySDK } from '@circadify/sdk';
const sdk = new CircadifySDK({
apiKey: 'ck_test_your_key_here',
onProgress: (progress) => {
// Update your custom progress UI
renderProgress(progress.percent);
},
onQualityWarning: (warning) => {
// Show quality feedback in your UI
renderWarning(warning.message);
},
});

In headless mode, the SDK accesses the camera but does not display the video. To show the camera feed in your own UI, obtain a MediaStream and assign it to a <video> element:

<video id="custom-video" autoplay playsinline muted></video>
// Request camera access and display in your element
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user', width: { ideal: 640 }, height: { ideal: 480 } },
});
const video = document.getElementById('custom-video');
video.srcObject = stream;
// Pass the stream to the SDK so it uses your camera feed
const result = await sdk.measureVitals({
demographics: { age: 35, sex: 'M' },
});

You control the video element entirely: size, position, CSS transforms (mirroring), overlays, and border styling are all yours to define.

Here is a complete example that ties together a custom camera feed, progress indicator, quality feedback, and results display:

<div class="scan-wrapper">
<video id="camera-feed" autoplay playsinline muted></video>
<div id="progress-ring"></div>
<p id="feedback-message"></p>
<div id="results-card" hidden></div>
<p id="error-message" hidden></p>
<button id="start-btn">Start Scan</button>
<button id="cancel-btn" hidden>Cancel</button>
</div>
import { CircadifySDK } from '@circadify/sdk';
const sdk = new CircadifySDK({
apiKey: 'ck_test_your_key_here',
onProgress: ({ percent, phase }) => {
renderProgressRing(percent);
renderFeedbackMessage(phase === 'calibrating' ? 'Hold still...' : 'Measuring...');
},
onQualityWarning: (warning) => {
renderFeedbackMessage(warning.message);
},
});
const controller = new AbortController();
document.getElementById('start-btn').addEventListener('click', async () => {
document.getElementById('start-btn').hidden = true;
document.getElementById('cancel-btn').hidden = false;
document.getElementById('error-message').hidden = true;
try {
const result = await sdk.measureVitals({
signal: controller.signal,
demographics: { age: 35, sex: 'M' },
});
renderResultsCard(result);
} catch (error) {
if (error.code !== 'CANCELLED') {
renderErrorState(error.message);
}
} finally {
document.getElementById('start-btn').hidden = false;
document.getElementById('cancel-btn').hidden = true;
}
});
document.getElementById('cancel-btn').addEventListener('click', () => {
controller.abort();
});
function renderProgressRing(percent) {
document.getElementById('progress-ring').style.background =
`conic-gradient(#3b82f6 ${percent * 3.6}deg, #e5e7eb 0deg)`;
}
function renderFeedbackMessage(message) {
document.getElementById('feedback-message').textContent = message;
}
function renderResultsCard(result) {
const card = document.getElementById('results-card');
card.hidden = false;
card.innerHTML = `
<p>Heart Rate: ${result.heartRate} BPM</p>
<p>SpO2: ${result.spo2 ?? '---'}%</p>
<p>Respiratory Rate: ${result.respiratoryRate ?? '---'} breaths/min</p>
<p>Confidence: ${(result.confidence * 100).toFixed(0)}%</p>
`;
}
function renderErrorState(message) {
const el = document.getElementById('error-message');
el.textContent = message;
el.hidden = false;
}

When building custom capture UIs, follow these guidelines to provide a reliable user experience:

  • Show a continuous progress indicator (ring, bar, or percentage) during the scan so users know the measurement is active.
  • Display real-time quality messages when the SDK reports issues (poor lighting, excessive movement, face not detected).
  • Provide a clear distinction between the “scanning” and “complete” states.
  • Ensure all interactive elements (start, cancel buttons) are keyboard-accessible and have visible focus indicators.
  • Use aria-live="polite" regions for progress and feedback text so screen readers announce updates without interrupting the user.
  • Pair color-based status indicators with text labels (do not rely on color alone).
  • Manage focus: move focus to the results area on completion, or to the error message on failure.
  • Use relative units or aspect-ratio for the video container so it scales across screen sizes.
  • On mobile devices, consider a full-viewport scan view with an overlay for feedback and controls.
  • Test on both portrait and landscape orientations.
  • Map SDK error codes to user-friendly messages that explain what went wrong and what to do next.
  • For retryable errors (timeouts, processing failures), offer a “Try Again” button that resets the UI and starts a new session.
  • For non-retryable errors (permission denied), guide the user to their browser or OS settings.