Detect when users get stuck — and respond in real time.
A lightweight SDK for understanding user behaviour and improving onboarding automatically.
- Minimal footprint: Zero runtime dependencies, ~20KB gzipped (ESM)
- No heavy dependencies: Framework-agnostic core, optional React overlay
- Fast initialization: Synchronous setup completes in ~10-15ms (config fetch adds network latency)
- Efficient event batching: Automatic batching and debouncing of events
- No PII captured automatically: Only explicit event payloads are sent
- Single audited transport: All outbound network calls flow through a single, auditable transport module
- Structured JSON only: No HTML injection, no dynamic code execution
- Passive rendering: Overlay renders text-only content from backend decisions
- Friction detection: Automatically detects user hesitation (stall), rapid clicks (rage click), and backward navigation (backtrack)
- Decision API: Backend determines when and which nudges to show
- Safe overlay rendering: Templates render decisions through auditable, text-only components
React applications:
npm install @reveal/client @reveal/overlay-reactOther frameworks (Vue, Svelte, Angular) or vanilla JavaScript:
npm install @reveal/client @reveal/overlay-wcNote:
@reveal/overlay-reactis a thin React adapter over@reveal/overlay-wcWeb Components. All UI logic lives in the framework-agnostic@reveal/overlay-wcpackage.
Create a RevealContextProvider component to handle SDK initialization and overlay rendering:
// components/RevealContextProvider.tsx (or app/reveal-context-provider.tsx)
'use client';
import React, { useEffect } from 'react';
import { Reveal, useNudgeDecision } from '@reveal/client';
import { OverlayManager } from '@reveal/overlay-react';
export function RevealContextProvider({ children }: { children: React.ReactNode }) {
const { decision, handlers } = useNudgeDecision();
useEffect(() => {
(async () => {
await Reveal.init('your-client-key');
})();
}, []);
return (
<>
{children}
<OverlayManager
decision={decision}
onDismiss={handlers.onDismiss}
onActionClick={handlers.onActionClick}
onTrack={handlers.onTrack}
/>
</>
);
}Then wrap your app with the provider:
Next.js (App Router):
// app/layout.tsx
import { RevealContextProvider } from './reveal-context-provider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<RevealContextProvider>{children}</RevealContextProvider>
</body>
</html>
);
}Next.js (Pages Router):
// pages/_app.tsx
import { RevealContextProvider } from '../components/RevealContextProvider';
export default function App({ Component, pageProps }) {
return (
<RevealContextProvider>
<Component {...pageProps} />
</RevealContextProvider>
);
}React (Create React App):
// src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { RevealContextProvider } from './components/RevealContextProvider';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<RevealContextProvider>
<App />
</RevealContextProvider>
);Use @reveal/overlay-wc Web Components directly:
import { Reveal } from '@reveal/client';
import '@reveal/overlay-wc';
// Initialize SDK
await Reveal.init('your-client-key');
// Create overlay manager
const manager = document.createElement('reveal-overlay-manager');
document.body.appendChild(manager);
// Subscribe to nudge decisions
Reveal.onNudgeDecision((decision) => {
manager.decision = decision;
});
// Listen for user interactions
manager.addEventListener('reveal:dismiss', (e) => {
Reveal.track('nudge', 'dismissed', { nudge_id: e.detail.id });
});
manager.addEventListener('reveal:action-click', (e) => {
Reveal.track('nudge', 'clicked', { nudge_id: e.detail.id });
});See overlay-wc/README.md for framework-specific examples (Vue, Svelte, etc.).
Track product events:
import { Reveal } from '@reveal/client';
// Track user actions with semantic IDs (recommended)
Reveal.track('product', 'button_clicked', {
action_id: 'signup_button_click',
flow_id: 'onboarding',
buttonId: 'signup',
page: '/onboarding',
});The OverlayManager component automatically displays contextual nudges when the backend decides to show them. No additional setup required.
For complete event type documentation, see docs/EVENTS.md.
- Structured payloads only: Only explicitly defined event payloads are sent. No automatic data collection.
- No DOM scraping: The SDK does not read or transmit DOM content, form values, or page HTML.
- Minimal storage usage: The SDK uses
localStorageandsessionStorageonly for SDK-internal state:localStorage: Anonymous ID persistence, treatment assignment, sampling decisions (all scoped per project + user)sessionStorage: Tab-level sequence counter for event ordering- All storage is SDK-internal state only (no user data stored)
- Storage failures are handled gracefully (fail-open behavior)
- Two documented transport modules: All network calls flow through two auditable modules:
packages/client/src/modules/transport.ts- JSON API calls (/ingest,/decide)packages/client/src/modules/recordingUpload.ts- Recording upload flow (3-step direct-to-storage)
- Text-only rendering: The overlay renders text content only. No HTML injection, no JavaScript execution, no dynamic code evaluation.
- No automatic PII capture: PII cannot be captured automatically. Only data explicitly passed to
Reveal.track()is sent.
The SDK operates in three layers:
- Detection Layer: Passive observers detect friction patterns (stall, rage click, backtrack)
- Decision Layer: Backend API determines when and which nudges to show
- Rendering Layer: OverlayManager renders backend decisions through safe, auditable templates
All layers are designed to fail gracefully and never break the host application.
- Dataflow & Architecture → docs/DATAFLOW.md
- SDK API Reference → docs/API.md
- Event Types → docs/EVENTS.md
- Security → docs/SECURITY.md
- Overlay Positioning → docs/OVERLAY_POSITIONING.md
- Audit Prompts → docs/AUDIT_AI.md
- Integration Prompts → docs/INTEGRATION_AI.md
MIT
