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.
Architecture
Section titled “Architecture”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() --------------------------->|- Your code — Renders the UI, handles user interaction, and displays all visual feedback
- Circadify SDK — Manages camera access, face detection, on-device processing, and result delivery
- Events — The SDK communicates state changes (progress, quality warnings, errors, results) via its event system
Setting Up
Section titled “Setting Up”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); },});Rendering the Camera Feed
Section titled “Rendering the Camera Feed”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 elementconst 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 feedconst 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.
Building the UI
Section titled “Building the UI”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;}Styling Guidelines
Section titled “Styling Guidelines”When building custom capture UIs, follow these guidelines to provide a reliable user experience:
Visual Feedback
Section titled “Visual Feedback”- 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.
Accessibility
Section titled “Accessibility”- 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.
Responsive Layout
Section titled “Responsive Layout”- Use relative units or
aspect-ratiofor 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.
Error Recovery
Section titled “Error Recovery”- 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.
Next Steps
Section titled “Next Steps”- React Integration — Use pre-built React components instead
- Events — Handle progress and quality events