Gestify lets visitors navigate your website using hand gestures through their webcam. It's privacy-first, needs no API key, and takes one line of code to integrate.
Gestify.init();The SDK handles everything β consent prompt, camera permission, gesture detection β automatically.
Most gesture libraries are complex. Gestify is not.
- No configuration required β works out of the box
- No API key, no server β fully offline capable
- No video leaves the browser β all processing is local
- No buttons to wire up β built-in consent modal
- No framework lock-in β works everywhere
npm install gestifyCDN (no build step required):
<script src="https://unpkg.com/gestify/dist/gestify.min.js"></script>import Gestify from 'gestify';
Gestify.init();<script src="https://unpkg.com/gestify/dist/gestify.min.js"></script>
<script>
Gestify.init();
</script>That's it. Gestify shows a consent prompt on first visit, remembers the user's choice, and handles everything automatically from there.
Choose a preset tuned for your use case:
Gestify.init({ profile: 'presentation' });| Profile | Best For | Speed | Hold Time |
|---|---|---|---|
default |
Most websites | 1Γ | 700 ms |
presentation |
Slides, demos | 1.3Γ | 650 ms |
accessibility |
Motor impairments | 0.8Γ | 1000 ms |
fast |
Power users | 1.7Γ | 500 ms |
gaming |
Interactive UIs | 2Γ | 400 ms |
Fine-tune individual parameters on top of any profile:
Gestify.init({
profile: 'default',
overrides: {
holdTime: 800, // ms to hold open-palm to enter cursor mode
exitHoldTime: 2000, // ms to hold fist to exit cursor mode
gestureSpeed: 1.2, // cursor speed multiplier
scrollSpeed: 1.0, // scroll speed multiplier
clickCooldown: 700, // ms between gesture-triggered clicks
cursorModeEnabled: true, // enable cursor-mode gestures
sensitivity: 'medium', // 'low' | 'medium' | 'high'
},
selector: 'button, a', // custom CSS selector for gesture targets
debug: true, // enable [Gestify] console logs
});Initializes Gestify and runs the full consent flow automatically.
| Scenario | What Gestify does |
|---|---|
| First visit | Shows a consent toast |
| Previously enabled | Silently restores camera |
| Camera already granted | Starts immediately |
| Camera blocked | Stays quiet |
| Dismissed this session | Stays quiet |
Programmatically start gestures, bypassing the built-in modal. Only needed if you're building a custom consent UI.
Stop gesture tracking. Camera is released. SDK stays initialized.
Full teardown β stops camera, removes all DOM elements, clears observers.
Gestify.enableCursorMode()
Gestify.disableCursorMode()
Gestify.toggleCursorMode()
Gestify.isCursorModeActive() // β boolean// components/GestureInit.jsx
import { useEffect } from 'react';
export default function GestureInit({ profile = 'default' }) {
useEffect(() => {
import('gestify').then(({ default: Gestify }) => Gestify.init({ profile }));
return () => import('gestify').then(({ default: Gestify }) => Gestify.destroy());
}, []);
return null;
}
// App.jsx
import GestureInit from './GestureInit';
export default function App() {
return <>
<GestureInit />
{/* rest of your app */}
</>;
}// components/GestureInit.jsx
'use client';
import { useEffect } from 'react';
export default function GestureInit({ profile = 'default' }) {
useEffect(() => {
import('gestify').then(({ default: Gestify }) => Gestify.init({ profile }));
return () => import('gestify').then(({ default: Gestify }) => Gestify.destroy());
}, []);
return null;
}App Router (app/layout.jsx):
import GestureInit from '@/components/GestureInit';
export default function RootLayout({ children }) {
return <html><body>{children}<GestureInit /></body></html>;
}Pages Router (_app.jsx):
import dynamic from 'next/dynamic';
const GestureInit = dynamic(() => import('../components/GestureInit'), { ssr: false });
export default function App({ Component, pageProps }) {
return <><Component {...pageProps} /><GestureInit /></>;
}<!-- components/GestureInit.vue -->
<template><!-- Gestify renders its own UI --></template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
const props = defineProps({ profile: { type: String, default: 'default' } });
onMounted(async () => {
const { default: Gestify } = await import('gestify');
Gestify.init({ profile: props.profile });
});
onUnmounted(async () => {
const { default: Gestify } = await import('gestify');
Gestify.destroy();
});
</script>App.vue:
<template>
<GestureInit />
<RouterView />
</template>
<script setup>
import GestureInit from '@/components/GestureInit.vue';
</script>// gesture-init.component.ts
import { Component, OnDestroy, Input } from '@angular/core';
@Component({ selector: 'app-gesture-init', standalone: true, template: '' })
export class GestureInitComponent implements OnDestroy {
@Input() profile = 'default';
private sdk: any = null;
constructor() {
if (typeof window === 'undefined') return; // SSR guard
import('gestify').then(({ default: Gestify }) => {
this.sdk = Gestify;
Gestify.init({ profile: this.profile });
});
}
ngOnDestroy() { this.sdk?.destroy(); }
}import Gestify from 'gestify';
import type { GestifyInitOptions } from 'gestify';
const options: GestifyInitOptions = {
profile: 'accessibility',
overrides: { holdTime: 1200, sensitivity: 'high' },
debug: true,
};
Gestify.init(options);
window.addEventListener('gestify:enabled', () => {
console.log('Gesture navigation active');
});
window.addEventListener('gestify:click', (e) => {
const { x, y } = e.detail; // fully typed
});Listen to Gestify events on window:
window.addEventListener('gestify:enabled', () => { /* started */ });
window.addEventListener('gestify:disabled', () => { /* stopped */ });
window.addEventListener('gestify:cursorModeEnter', () => { /* cursor on */ });
window.addEventListener('gestify:cursorModeExit', (e) => console.log(e.detail.reason));
window.addEventListener('gestify:click', (e) => console.log(e.detail)); // { x, y }
window.addEventListener('gestify:scroll', (e) => console.log(e.detail)); // { direction, amount }
window.addEventListener('gestify:permissionDenied', (e) => console.log(e.detail.reason));
window.addEventListener('gestify:consentDismissed', () => { /* user clicked No */ });
window.addEventListener('gestify:error', (e) => console.error(e.detail.message));| Browser | Minimum Version |
|---|---|
| Chrome | 90+ |
| Edge | 90+ |
| Firefox | 90+ |
| Safari | 15+ |
HTTPS required. Camera access is restricted to secure origins.
localhostworks in development.
Desktop only. Mobile and tablet devices are not supported β gesture control requires a desktop webcam.
Gestify is built privacy-first:
| π All processing is local | Powered by MediaPipe β runs entirely in the browser |
| π« No video transmitted | Camera frames never leave the device |
| π« No analytics | Zero data collection |
| π« No background access | Camera only activates after explicit user consent |
| β User is in control | Can disable at any time; consent persists across sessions |
Gestify is compliant with GDPR, CCPA, and similar privacy regulations.
Camera permission denied
const state = await Gestify.getPermissionState();
// If 'denied' β ask user to reset via browser Settings β Privacy β CameraNo gestures detected
- Ensure good lighting (avoid strong backlight)
- Keep hand 30β60 cm from camera
- Try
profile: 'accessibility'for more forgiving thresholds - Enable
debug: trueand check the browser console
SSR errors (Next.js, Nuxt, Angular Universal)
Always use a dynamic import β Gestify is browser-only:
// β
correct
import('gestify').then(({ default: Gestify }) => Gestify.init());
// β incorrect β will break SSR
import Gestify from 'gestify';Content Security Policy (CSP)
Gestify loads MediaPipe from jsDelivr. Allow it:
Content-Security-Policy: script-src 'self' https://cdn.jsdelivr.net;Pull requests are welcome. See CONTRIBUTING.md to get started.
See CHANGELOG.md.
MIT Β© Gestify Contributors