Sekyro AIDocs

@sekyro/react

React components: <LivenessCapture> and the full <SekyroVerification> flow.

React component for Sekyro AI liveness capture: the hosted page's capture UI (<LivenessCapture />), extracted onto the public @sekyro/web API with no private design-system or framework dependency. Themeable through CSS variables, ar / fr / en built in, RTL-safe (logical CSS only), WCAG AAA defaults.

Status: 0.1.0-rc.0, still private — the public npm release is wired in a later task (Changesets + Trusted Publishing). Sekyro AI's own hosted page dogfoods it via a file: link.

Install

npm install @sekyro/react @sekyro/web react react-dom

Peer dependencies: react >=18.2, react-dom >=18.2, @sekyro/web ^0.1.0-rc.8 || ^0.2.0-rc.0. No other runtime dependencies.

Usage

import { createVerification } from "@sekyro/web";
import { LivenessCapture, SekyroThemeProvider } from "@sekyro/react";
import "@sekyro/react/styles.css";

const verification = createVerification({ clientToken, locale: "ar" });

<SekyroThemeProvider theme={{ colors: { brand: "#0c7a3d", ring: "#0c7a3d" }, radius: "8px" }}>
  <LivenessCapture
    verification={verification}
    locale="ar"
    onResult={(r) => console.log(r.status, r.reason)}
  />
</SekyroThemeProvider>
  • Verification mode (verification set): the handle comes from verification.liveness(); after captured the SDK uploads, attests and submits the liveness + face_match checks and the verdict arrives via onResult (approved | rejected | review | retry | unavailable). On retry the SDK re-arms the camera by itself (rearm: 'auto') while the component shows the reason, the retry coaching and the remaining attempts; with createVerification({ rearm: 'manual' }) the component instead offers a focused "Start a new attempt" button.
  • The still freezes the stage the instant recording stops (the handle's frozen event) — before the clip finalizes; the "camera off" pill only appears once the tracks are actually stopped (captured). A LOW device profile (profile event) drops the decorative animations.
  • Capture-only mode (no verification): createLivenessCapture(); the component emits onCaptured(artifacts) (keyframe + clip blobs) and offers a retake. You own the upload.

Props

PropTypeNotes
verification?VerificationFrom createVerification(). Absent → capture-only.
locale"ar" | "fr" | "en"Built-in dictionary.
dir?"ltr" | "rtl"Defaults to rtl for ar.
strings?Partial<Strings>Per-key overrides on top of the built-ins.
slots?{ header, footer, intro(start, context), stageOverlay(videoRef), voicePrompt(hint, text, key, voice), errorPanel(err, retry) }intro replaces the built-in pre-capture screen (see below); stageOverlay renders inside the stage box above the ring (overlays, debug HUDs); voicePrompt replaces the built-in voice chip in the stage's start corner; errorPanel replaces the camera-error notice.
autoStart?boolean | "when-permitted"Skip the pre-capture screen and start once the detector preload is ready — at most once per mount. true arms it (flip it when your own preconditions are met); "when-permitted" arms it only when the camera permission is already granted (until the permission probe answers, the screen shows its loader).
notice?ReactNode | { tone, text }Host message above the pre-capture screen (busy service, previous failure). Pre-capture only.
startDisabled? / startLabel?boolean / ReactNodeDisable the built-in Start and relabel it (e.g. a cooldown: "Retry in 12 s"). It gates only the pre-capture Start — not the error Retry, "record again" or the retake button.
tips?false | (BuiltInTipId | IntroTip)[]Tips card; default DEFAULT_TIPS; false hides it.
defaultFacing?"user" | "environment"Camera the attempt opens with (default front).
voice?false | VoiceOptionsVoice guidance; on by default. false = no speech, no switch, no chip.
onGuidance?(key) => voidTelemetry: once per change of the displayed fcv3.* guidance key.
onStateChange?(state) => voidEvery capture-state transition, for host chrome (hide a description while the stage is live, telemetry).
onCaptured?(artifacts) => voidFires before any upload.
onResult?(result) => voidVerification mode only.
onError?(error) => voidCamera / engine failure (permission | notfound | inuse | insecure | unsupported | generic).
assetBaseUrl?stringDetector assets base (default /models-v3).

Pre-capture screen

While the capture is idle the component renders a built-in "get ready" screen: a tips card, the camera-permission row (granted / prompt / denied with the fix / unknown), a front/back choice on devices with both cameras, the voice switch, the security note and one Start control gated on the detector preload (preloadLivenessAssets from @sekyro/web, kicked on mount): a busy Start while the models download, Start when ready, a danger notice + Retry if the download failed. Without a camera API (SSR, old browsers) the preload is skipped and Start is ready at once.

slots.intro(start, context) replaces the whole screen. context is { assets: "loading" | "ready" | "failed", retryAssets, permission, facing, setFacing, canChooseFacing, voice }; call start from your click handler — it opens context.facing and unlocks audio inside the gesture (iOS). The notice still renders above your intro.

<LivenessCapture
  verification={verification}
  locale="fr"
  notice={busy ? { tone: "warning", text: t("serviceBusy") } : undefined}
  startDisabled={cooldown > 0}
  startLabel={cooldown > 0 ? t("retryIn", { s: cooldown }) : undefined}
  tips={["light", "uncover", { id: "id-card", text: t("haveIdReady") }]}
/>

Voice guidance

During capture the component speaks the displayed (debounced) guidance — the fcv3.* gate hints — through one <audio> element, falling back to the device voice (speechSynthesis). A stage chip (aria-pressed) and the intro switch toggle it; the viewer's choice persists in localStorage.

VoiceOptionsDefaultNotes
defaultEnabled?trueUsed when the viewer has no stored preference.
clips?verification mode: Sekyro AI clips from the session (add https://assets.sekyro.ai to media-src); capture-only: noneVoiceClipSource: a resolver (key, locale) => url | null or a map { [locale]: { [voiceKey]: url } } (e.g. delivered by your backend — pass a new object to change it). Yours always wins over the Sekyro AI clips. A missing URL or a throwing resolver = no clip → device voice.
speechFallback?truefalse, or the locales where the device voice may speak. A device with no voice for the language stays silent rather than read Arabic in a French voice.
storageKey?"sekyro.voiceGuidance"null = do not persist.

Sekyro AI clips. In verification mode the component speaks the recorded ar/fr clip set that arrives with the session (verification.voiceClips()), served from https://assets.sekyro.ai — a page with a Content-Security-Policy must allow that origin in media-src. Until the set arrives, when the session has none, or when it cannot be fetched, the device voice speaks. The package ships no audio. VOICE_KEYS lists the nine fcv3.* guidance keys plus capture.tips.summary.

"Listen to the instructions". The pre-capture screen has a button under the tips that plays the spoken summary capture.tips.summary. It never auto-plays, a second tap stops it, and Start stops it too. It shows only with the built-in tips, unless your voice.clips has a capture.tips.summary clip for the locale. It never shows with voice={false}, and the voice switch does not hide it.

iOS. Audio unlocks inside a user gesture: the Start tap, or the voice switch/chip turned on. With autoStart there is no Start tap, so audio stays locked until the person's first activating touch, click or key press anywhere inside the component (touchend / click / keydown other than Escape, or a mouse/pen pointerup; a touch pointerdown is not a user activation on WebKit). It unlocks once; an event without activation leaves the listener armed. The clips are prefetched in that same gesture, and only while voice is on. Guidance due before the unlock stays silent on iOS.

voicePrompt slot. It replaces the chip and receives the shared VoiceControl ({ supported, enabled, setEnabled, toggle }) as its fourth argument; the component keeps speaking. If your slot plays audio itself, pass voice={false} or the person hears it twice. A development build warns once when slots.voicePrompt is set and voice is undefined (pass voice={{}} to keep the built-in voice and silence it).

CSP. Clips and the unlock use media-src 'self' blob: (the silent unlock WAV is a blob: URL — no data: needed); add your clip host if clips points off-origin.

Theme

SekyroThemeProvider writes CSS variables on a .sekyro-root wrapper: --sekyro-color-{brand,brand-fg,fg,fg-muted,bg,surface,ring,danger,success,warning}, --sekyro-radius, --sekyro-font. Without a provider the stylesheet's :root defaults apply (DEFAULT_THEME). You can also set the variables yourself on any ancestor.

Contrast rules the component relies on (checked for the default theme in test/contrast.test.ts):

  • fg on surface / bg, brandFg on brand: text → 7:1 (AAA);
  • surface on fg: the stage scrim is fg and the guidance line is surface, so the on-camera text inherits the same ratio;
  • fgMuted on surface: secondary text only (security note, checking hint) — AA (4.5:1) is the floor, AAA recommended;
  • brand, ring, danger, warning, success on surface: non-text UI → 3:1.

Strings

Keys follow the hosted message catalog (capture.*, fcv2.err.*, fcv3.*, reasons.*, errors.attestation_invalid, errors.network_error, common.retry). hintStringKey(hint) maps a GateHint code to its guidance key; reasonStringKey(reason) maps a result reason to its copy key.

Accessibility

  • One always-mounted aria-live="polite" guidance line is the sole announcement channel during capture; failures use role="alert".
  • Every interactive element is ≥ 48 px with a 3 px --sekyro-color-ring focus ring.
  • prefers-reduced-motion disables all stage animations; the sekyro-capture-reduce class forces the same for low-end devices.
  • Inline SVG icons only; no emoji; logical CSS properties only (scripts/check-css.mjs fails the build on any physical left/right).

Entries: @sekyro/react and @sekyro/react/verification

The main entry is the 0.1 liveness component above. The profile v2 steps (<SekyroVerification>, <PhoneOtpStep>, <MethodChoice>, <NniEntry>, <DocumentCapture>, <VerificationResultScreen>) live on a second entry, @sekyro/react/verification, with its own stylesheet (import "@sekyro/react/verification.css" next to styles.css). It needs @sekyro/web 0.1.0-rc.8 or later (the plan runner).

Use the ESM build (import). In ESM both entries import one shared copy of the common code (theme, strings, <LivenessCapture>), and the document camera view is a separate chunk loaded only when a document step shows. The CJS build (require) has no code splitting: each entry is a self-contained bundle with its own copy of the shared modules, so nothing module-level is shared between @sekyro/react and @sekyro/react/verification (for example, a SekyroThemeProvider or <LivenessCapture> imported from one entry is a different component from the one the other entry renders), the camera view is inlined, and the code is downloaded twice if you import both. The theme itself is plain CSS variables on .sekyro-root, so a provider from either entry still themes both, but every bundler partners use (Next, Vite, webpack 5, esbuild) resolves the ESM build by default, so keep it that way.

Scripts

npm test (CSS lint + vitest: behaviour, jest-axe per state × ar/fr, contrast, RTL snapshot) · npm run typecheck · npm run build (tsup ESM + CJS + d.ts + dist/styles.css) · npm run api:check (api-extractor against etc/sekyro-react.api.md).

On this page