🎉 @facesmash/sdk v0.1.0 is now available on npm — Read the docs →
FaceSmash Docs
JavaScript SDK

React Components

Pre-built React components and hooks for face registration and login

The React bindings provide drop-in components and hooks for adding face authentication to any React application. Everything is exported from the @facesmash/sdk/react entry point.

Installation

npm install @facesmash/sdk

React 17+ and React DOM 17+ are required as peer dependencies.

// All React imports come from a single entry point
import {
  FaceSmashProvider,
  FaceLogin,
  FaceRegister,
  useFaceSmash,
  useFaceLogin,
  useFaceRegister,
  useFaceAnalysis,
} from '@facesmash/sdk/react';

<FaceSmashProvider>

Context provider that creates the FaceSmashClient instance, initializes the WebGL backend, and loads all five neural networks. You must wrap your face auth UI with this provider — all other components and hooks depend on it.

The provider creates the client once on mount and holds it in a ref, so re-renders don't re-create the client. Model loading starts immediately on mount.

Basic Usage

import { FaceSmashProvider } from '@facesmash/sdk/react';

function App() {
  return (
    <FaceSmashProvider
      config={{ apiUrl: 'https://api.facesmash.app' }}
      onReady={() => console.log('Models loaded!')}
      onError={(err) => console.error('Init failed:', err)}
    >
      {/* Your face auth UI goes here */}
    </FaceSmashProvider>
  );
}

With Global Event Listener

import { FaceSmashProvider } from '@facesmash/sdk/react';
import type { FaceSmashEvent } from '@facesmash/sdk';

function handleEvent(event: FaceSmashEvent) {
  switch (event.type) {
    case 'models-loading':
      console.log(`Loading models: ${event.progress}%`);
      break;
    case 'models-loaded':
      console.log('All models ready');
      break;
    case 'login-success':
      console.log(`User ${event.user.name} logged in (similarity: ${event.similarity})`);
      break;
    case 'login-failed':
      console.warn(`Login failed: ${event.error}`);
      break;
    case 'register-success':
      console.log(`New user registered: ${event.user.name}`);
      break;
    case 'register-failed':
      console.warn(`Registration failed: ${event.error}`);
      break;
  }
}

function App() {
  return (
    <FaceSmashProvider
      config={{ apiUrl: 'https://api.facesmash.app', debug: true }}
      onEvent={handleEvent}
    >
      {/* ... */}
    </FaceSmashProvider>
  );
}

Props

PropTypeDefaultDescription
childrenReactNoderequiredChild components that need face auth
configFaceSmashConfig{}SDK configuration — see Configuration for all options
onReady() => voidundefinedFires once when all 5 neural networks finish loading
onError(error: string) => voidundefinedFires if model loading fails (e.g., network error, WebGL unavailable)
onEvent(event: FaceSmashEvent) => voidundefinedFires for every SDK event — models, login, register, face detection

Context Value

The provider exposes these values to child components via useFaceSmash():

FieldTypeDescription
clientFaceSmashClientThe initialized SDK client — access client.login(), client.register(), client.analyzeFace(), client.on(), client.pb (PocketBase instance)
isReadybooleantrue once all models are loaded and the client can process faces
isLoadingbooleantrue while models are downloading and initializing
loadProgressnumberProgress value from 0 to 100 during model loading
errorstring | nullError message if model loading failed, otherwise null
retryInit() => voidCall this to retry model loading after a failure

Do not nest multiple providers. Each FaceSmashProvider creates its own FaceSmashClient and PocketBase instance. Use a single provider at the top of your component tree.


<FaceLogin />

Drop-in login component. Renders a mirrored webcam feed, waits 2 seconds for the user to position their face, then auto-captures multiple frames and authenticates against all registered users in PocketBase.

Basic Usage

import { FaceSmashProvider, FaceLogin } from '@facesmash/sdk/react';

function LoginPage() {
  return (
    <FaceSmashProvider config={{ apiUrl: 'https://api.facesmash.app' }}>
      <FaceLogin
        onResult={(result) => {
          if (result.success) {
            console.log('Welcome back,', result.user.name);
            console.log('Match similarity:', result.similarity);
            window.location.href = '/dashboard';
          } else {
            console.error('Login failed:', result.error);
          }
        }}
        className="w-full h-80 rounded-xl overflow-hidden"
      />
    </FaceSmashProvider>
  );
}

With Custom Overlay and Error Handling

import { FaceSmashProvider, FaceLogin } from '@facesmash/sdk/react';

function LoginPage() {
  return (
    <FaceSmashProvider config={{ apiUrl: 'https://api.facesmash.app' }}>
      <FaceLogin
        onResult={(result) => {
          if (result.success) {
            window.location.href = '/dashboard';
          }
        }}
        captureCount={5}
        captureDelay={300}
        className="w-96 h-72 rounded-2xl overflow-hidden shadow-xl"
        overlay={
          <div className="absolute inset-0 flex items-end justify-center pb-4">
            <div className="bg-black/60 px-4 py-2 rounded-full text-white text-sm">
              Position your face in the frame
            </div>
          </div>
        }
        loadingContent={
          <div className="flex items-center justify-center h-72">
            <div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" />
            <span className="ml-3 text-gray-500">Loading face recognition...</span>
          </div>
        }
        errorContent={(error, retry) => (
          <div className="flex flex-col items-center justify-center h-72 gap-4">
            <p className="text-red-500">{error}</p>
            <button onClick={retry} className="px-4 py-2 bg-blue-500 text-white rounded-lg">
              Try Again
            </button>
          </div>
        )}
      />
    </FaceSmashProvider>
  );
}

Manual Start (No Auto-Scan)

import { useState } from 'react';
import { FaceSmashProvider, FaceLogin } from '@facesmash/sdk/react';

function LoginPage() {
  const [showCamera, setShowCamera] = useState(false);

  return (
    <FaceSmashProvider config={{ apiUrl: 'https://api.facesmash.app' }}>
      {!showCamera ? (
        <button onClick={() => setShowCamera(true)}>
          Sign in with Face
        </button>
      ) : (
        <FaceLogin
          autoStart={false}
          onResult={(result) => {
            setShowCamera(false);
            if (result.success) {
              alert(`Welcome back, ${result.user.name}!`);
            }
          }}
        />
      )}
    </FaceSmashProvider>
  );
}

Props

PropTypeDefaultDescription
onResult(result: LoginResult) => voidrequiredCalled when authentication completes (success or failure)
captureCountnumber3Number of webcam frames to capture. More frames = higher accuracy, slower
captureDelaynumber500Milliseconds between each frame capture
autoStartbooleantrueIf true, scanning begins automatically 2 seconds after the camera is ready
classNamestringundefinedCSS class applied to the outermost <div> container
overlayReactNodeundefinedRendered as an absolute-positioned layer on top of the video feed
loadingContentReactNodeundefinedReplaces the entire component while models are loading. If not provided, the video element is rendered (but may be blank)
errorContent(error: string, retry: () => void) => ReactNodeundefinedReplaces the entire component when an error occurs. Receives the error message and a retry function

LoginResult Type

interface LoginResult {
  /** Whether the login succeeded */
  success: boolean;
  /** The matched user profile (only present on success) */
  user?: UserProfile;
  /** Similarity score between 0 and 1 (only present on success or partial match) */
  similarity?: number;
  /** Human-readable error message (only present on failure) */
  error?: string;
}

interface UserProfile {
  id: string;           // PocketBase record ID
  name: string;         // Display name
  email: string;        // Email address
  face_embedding: number[]; // 128-dimensional face descriptor
  created: string;      // ISO date string
  updated: string;      // ISO date string
}

Component Lifecycle

  1. Loading — Models are being downloaded (isLoading=true). Shows loadingContent if provided.
  2. Ready — Models loaded, camera access requested and granted. Status = ready.
  3. Scanning — After 2-second delay (if autoStart), captures captureCount frames at captureDelay intervals. Status = scanning.
  4. Done — client.login(images) resolves. onResult is called with the result. Status = done.
  5. Error — Camera denied or model loading failed. Shows errorContent if provided, otherwise shows a default error overlay with retry button.

<FaceRegister />

Drop-in registration component. Renders a mirrored webcam feed, captures face images, checks for duplicate faces among existing users, and creates a new user profile in PocketBase.

Basic Usage

import { useState } from 'react';
import { FaceSmashProvider, FaceRegister } from '@facesmash/sdk/react';

function RegisterPage() {
  const [name, setName] = useState('');

  return (
    <FaceSmashProvider config={{ apiUrl: 'https://api.facesmash.app' }}>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Your name"
        className="mb-4 px-4 py-2 border rounded"
      />
      <FaceRegister
        name={name}
        email="user@example.com"
        onResult={(result) => {
          if (result.success) {
            console.log('Registered:', result.user.name);
            console.log('User ID:', result.user.id);
            window.location.href = '/login';
          } else {
            console.error('Registration failed:', result.error);
          }
        }}
        className="w-full h-80 rounded-xl overflow-hidden"
      />
    </FaceSmashProvider>
  );
}

With Full Customization

<FaceRegister
  name="Jane Doe"
  email="jane@example.com"
  captureCount={5}
  captureDelay={400}
  autoStart={false}
  className="w-96 h-72 rounded-2xl border-2 border-dashed border-gray-300"
  overlay={
    <div className="absolute top-4 left-0 right-0 text-center">
      <span className="bg-green-500/80 text-white px-3 py-1 rounded-full text-xs">
        Look straight at the camera
      </span>
    </div>
  }
  loadingContent={
    <div className="h-72 flex items-center justify-center text-gray-400">
      Initializing face recognition engine...
    </div>
  }
  errorContent={(error, retry) => (
    <div className="h-72 flex flex-col items-center justify-center gap-3">
      <p className="text-red-400 text-sm">{error}</p>
      <button onClick={retry} className="text-blue-400 underline text-sm">Retry</button>
    </div>
  )}
  onResult={(result) => {
    if (result.success) {
      alert(`Welcome, ${result.user.name}! You can now sign in with your face.`);
    } else {
      alert(`Registration failed: ${result.error}`);
    }
  }}
/>

Props

PropTypeDefaultDescription
namestringrequiredDisplay name for the new user
emailstringundefinedEmail address. If not provided, auto-generated as name.lowercase@facesmash.app
onResult(result: RegisterResult) => voidrequiredCalled when registration completes
captureCountnumber3Number of webcam frames to capture
captureDelaynumber500Milliseconds between each frame capture
autoStartbooleantrueIf true, capture begins 2 seconds after the camera is ready
classNamestringundefinedCSS class for the container
overlayReactNodeundefinedAbsolute-positioned layer on top of the video
loadingContentReactNodeundefinedShown while models load
errorContent(error: string, retry: () => void) => ReactNodeundefinedShown on error

RegisterResult Type

interface RegisterResult {
  /** Whether registration succeeded */
  success: boolean;
  /** The newly created user profile (only present on success) */
  user?: UserProfile;
  /** Human-readable error message (only present on failure) */
  error?: string;
}

Possible Errors

ErrorCause
"No face detected in any image"No face found in any of the captured frames
"Face quality too low for registration."Face detected but quality score below minQualityScore threshold
"This face is already registered to {name}"A face with ≥75% similarity already exists in the database
"Camera access denied or not available"User denied camera permission or no camera detected

Component Lifecycle

  1. Loading — Models downloading. Shows loadingContent if provided.
  2. Ready — Camera active, waiting to capture.
  3. Capturing — Taking captureCount frames at captureDelay intervals.
  4. Done — client.register(name, images, email) resolves. onResult called.
  5. Error — Camera denied, no face found, duplicate face, or quality too low.

Hooks

All hooks must be called within a <FaceSmashProvider>. They throw an error if used outside the provider context.

useFaceSmash()

The foundational hook. Gives you direct access to the FaceSmashClient instance and all loading state. Use this when you need full control or want to call client methods directly.

import { useFaceSmash } from '@facesmash/sdk/react';

function StatusBar() {
  const { isReady, isLoading, loadProgress, error, retryInit } = useFaceSmash();

  if (error) {
    return (
      <div className="text-red-500">
        Failed to load: {error}
        <button onClick={retryInit} className="ml-2 underline">Retry</button>
      </div>
    );
  }

  if (isLoading) {
    return <div>Loading face recognition models... {loadProgress}%</div>;
  }

  if (isReady) {
    return <div className="text-green-500">Face recognition ready</div>;
  }

  return null;
}

Accessing the Client Directly

function CustomLoginButton() {
  const { client, isReady } = useFaceSmash();

  const handleLogin = async () => {
    if (!isReady) return;

    // You have full access to the FaceSmashClient
    const analysis = await client.analyzeFace(someBase64Image);
    console.log('Quality:', analysis?.qualityScore);
    console.log('Head pose:', analysis?.headPose);

    // Or trigger a full login
    const result = await client.login([image1, image2, image3]);
    console.log(result);
  };

  return <button onClick={handleLogin} disabled={!isReady}>Sign In</button>;
}

Return Values

FieldTypeDescription
clientFaceSmashClientThe SDK client — call .login(), .register(), .analyzeFace(), .on(), access .pb (PocketBase), .config, .isReady
isReadybooleantrue when all 5 neural networks are loaded and ready
isLoadingbooleantrue during model download and initialization
loadProgressnumber0–100 progress value. Jumps to 10 after TF.js init, then 100 when all models load
errorstring | null"Failed to load face recognition models" on failure, otherwise null
retryInit() => voidResets error state and re-attempts model loading

useFaceLogin()

A convenience hook that wraps client.login() with React state management. Use this when you're building a custom login UI and want to manage the webcam yourself.

import { useFaceLogin } from '@facesmash/sdk/react';

function CustomLoginFlow() {
  const { login, isScanning, result, reset, isReady } = useFaceLogin();

  const handleCapture = async (images: string[]) => {
    const loginResult = await login(images);

    if (loginResult.success) {
      console.log('Authenticated as:', loginResult.user.name);
      console.log('Similarity:', loginResult.similarity);
    } else {
      console.error(loginResult.error);
    }
  };

  return (
    <div>
      {isScanning && <p>Analyzing face...</p>}
      {result?.success && <p>Welcome back, {result.user.name}!</p>}
      {result && !result.success && (
        <div>
          <p>Login failed: {result.error}</p>
          <button onClick={reset}>Try Again</button>
        </div>
      )}
    </div>
  );
}

Return Values

FieldTypeDescription
login(images: string[]) => Promise<LoginResult>Pass an array of base64 data URLs. Returns the login result and also updates result state
isScanningbooleantrue while login() is in progress
resultLoginResult | nullThe most recent login result, or null before first attempt
reset() => voidClears result and isScanning — use for "try again" flows
isReadybooleantrue when models are loaded (mirrors useFaceSmash().isReady)

useFaceRegister()

Wraps client.register() with React state. Build a custom registration UI while the hook manages loading/result state.

import { useState } from 'react';
import { useFaceRegister } from '@facesmash/sdk/react';

function CustomRegistration() {
  const { register, isRegistering, result, reset, isReady } = useFaceRegister();
  const [name, setName] = useState('');

  const handleCapture = async (images: string[]) => {
    if (!name.trim()) {
      alert('Please enter your name');
      return;
    }

    const regResult = await register(name, images, 'user@example.com');

    if (regResult.success) {
      console.log('User created:', regResult.user.id);
      console.log('Email:', regResult.user.email);
    } else {
      console.error(regResult.error);
    }
  };

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" />
      {isRegistering && <p>Registering face...</p>}
      {result?.success && <p>Welcome, {result.user.name}! You can now sign in.</p>}
      {result && !result.success && (
        <div>
          <p>Failed: {result.error}</p>
          <button onClick={reset}>Try Again</button>
        </div>
      )}
    </div>
  );
}

Return Values

FieldTypeDescription
register(name: string, images: string[], email?: string) => Promise<RegisterResult>Creates a new user with the given name and face images
isRegisteringbooleantrue while register() is in progress
resultRegisterResult | nullThe most recent registration result
reset() => voidClears result and isRegistering
isReadybooleantrue when models are loaded

useFaceAnalysis()

Analyze a single face image without logging in or registering. Returns detailed quality metrics including lighting, head pose, eye openness, and detection confidence. Useful for building quality-check UIs, preview screens, or real-time face tracking overlays.

import { useFaceAnalysis } from '@facesmash/sdk/react';

function QualityChecker() {
  const { analyze, analysis, isAnalyzing, isReady } = useFaceAnalysis();

  const checkQuality = async (imageData: string) => {
    const result = await analyze(imageData);
    if (!result) {
      console.log('No face detected');
      return;
    }

    console.log('Quality score:', result.qualityScore);      // 0-1
    console.log('Confidence:', result.confidence);            // 0-1
    console.log('Lighting:', result.lightingScore);           // 0-1
    console.log('Head pose:', result.headPose);               // { yaw, pitch, roll, isFrontal }
    console.log('Eyes open:', result.eyeAspectRatio > 0.2);   // EAR threshold
    console.log('Face size:', result.faceSizeCheck);          // { isValid, ratio, reason? }
    console.log('Rejection reason:', result.rejectionReason); // string or undefined
  };

  return (
    <div>
      {isAnalyzing && <p>Analyzing...</p>}
      {analysis && (
        <div>
          <p>Quality: {(analysis.qualityScore * 100).toFixed(0)}%</p>
          <p>Lighting: {(analysis.lightingScore * 100).toFixed(0)}%</p>
          <p>Head pose: {analysis.headPose.isFrontal ? 'Frontal' : 'Tilted'}</p>
          <p>Face size valid: {analysis.faceSizeCheck.isValid ? 'Yes' : 'No'}</p>
        </div>
      )}
    </div>
  );
}

Return Values

FieldTypeDescription
analyze(imageData: string) => Promise<FaceAnalysis | null>Analyze a base64 data URL. Returns null if no face is detected
analysisFaceAnalysis | nullThe most recent analysis result
isAnalyzingbooleantrue while analysis is running
isReadybooleantrue when models are loaded

FaceAnalysis Type

interface FaceAnalysis {
  /** Raw 128-dimensional face descriptor */
  descriptor: Float32Array;
  /** L2-normalized face descriptor */
  normalizedDescriptor: Float32Array;
  /** SSD MobileNet detection confidence (0-1) */
  confidence: number;
  /** Composite quality score factoring in lighting, size, and pose (0-1) */
  qualityScore: number;
  /** Lighting quality score (0-1, higher = better lit) */
  lightingScore: number;
  /** Head orientation */
  headPose: {
    yaw: number;     // Left/right rotation (-1 to 1)
    pitch: number;   // Up/down tilt (-1 to 1)
    roll: number;    // Clockwise rotation (radians)
    isFrontal: boolean; // true if |yaw| < 0.35 && |pitch| < 0.4 && |roll| < 0.25
  };
  /** Face size relative to frame */
  faceSizeCheck: {
    isValid: boolean;  // true if face is 2-65% of frame area and >= 80x80 px
    ratio: number;     // Face area / frame area
    reason?: string;   // "Face too far", "Face too close", etc.
  };
  /** Eye aspect ratio (higher = more open, ~0.2-0.35 for open eyes) */
  eyeAspectRatio: number;
  /** If present, this face was rejected and should not be used for matching */
  rejectionReason?: string;
}

Full Example: Login + Register App

import { useState } from 'react';
import {
  FaceSmashProvider,
  FaceLogin,
  FaceRegister,
} from '@facesmash/sdk/react';
import type { FaceSmashEvent } from '@facesmash/sdk';

function App() {
  const [page, setPage] = useState<'login' | 'register'>('login');
  const [user, setUser] = useState<{ name: string } | null>(null);

  const handleEvent = (event: FaceSmashEvent) => {
    if (event.type === 'models-loading') {
      console.log(`Loading: ${event.progress}%`);
    }
  };

  if (user) {
    return <div>Welcome, {user.name}! <button onClick={() => setUser(null)}>Logout</button></div>;
  }

  return (
    <FaceSmashProvider
      config={{ apiUrl: 'https://api.facesmash.app', debug: true }}
      onEvent={handleEvent}
    >
      <div style={{ maxWidth: 400, margin: '0 auto' }}>
        <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
          <button onClick={() => setPage('login')} style={{ fontWeight: page === 'login' ? 'bold' : 'normal' }}>
            Login
          </button>
          <button onClick={() => setPage('register')} style={{ fontWeight: page === 'register' ? 'bold' : 'normal' }}>
            Register
          </button>
        </div>

        {page === 'login' ? (
          <FaceLogin
            onResult={(result) => {
              if (result.success) setUser({ name: result.user.name });
              else alert(result.error);
            }}
            className="w-full h-72 rounded-xl overflow-hidden bg-black"
          />
        ) : (
          <FaceRegister
            name="New User"
            onResult={(result) => {
              if (result.success) {
                alert(`Registered as ${result.user.name}`);
                setPage('login');
              } else {
                alert(result.error);
              }
            }}
            className="w-full h-72 rounded-xl overflow-hidden bg-black"
          />
        )}
      </div>
    </FaceSmashProvider>
  );
}

Troubleshooting

"useFaceSmash must be used within a <FaceSmashProvider>"

All hooks and components require the provider. Ensure your component tree looks like:

<FaceSmashProvider>
  <YourComponent />  {/* hooks work here */}
</FaceSmashProvider>

Camera not starting

  • HTTPS required — getUserMedia only works on https:// or localhost
  • Permissions — The user must grant camera access when prompted
  • Mobile Safari — Requires a user gesture (tap) before camera access

Models loading slowly

  • First load downloads ~12.5 MB of neural network weights
  • Subsequent loads use the browser's HTTP cache
  • Consider showing a loading bar using useFaceSmash().loadProgress

autoStart not working

The component waits 2 seconds after the camera is ready before starting capture. This delay allows the user to position their face. If you set autoStart={false}, you must trigger scanning programmatically or re-mount the component.

On this page