@jashwanthj/gestify
v1.0.0
Published
Add gesture navigation to your website with ease.
Maintainers
Readme
Gestify
Add gesture navigation to your website with ease.
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.
Why Gestify
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
Installation
npm install @jashwanthj/gestifyCDN (no build step required):
<script src="https://unpkg.com/@jashwanthj/gestify/dist/gestify.min.js"></script>Quick Start
npm / ESM
import Gestify from '@jashwanthj/gestify';
Gestify.init();CDN / Script tag
<script src="https://unpkg.com/@jashwanthj/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.
Profiles
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 |
Configuration
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
});API Reference
Gestify.init(options?) — start here
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 |
Gestify.enable() — advanced
Programmatically start gestures, bypassing the built-in modal. Only needed if you're building a custom consent UI.
Gestify.disable()
Stop gesture tracking. Camera is released. SDK stays initialized.
Gestify.destroy()
Full teardown — stops camera, removes all DOM elements, clears observers.
Gestify.isEnabled() → boolean
Gestify.getPermissionState() → Promise<'granted' | 'denied' | 'prompt' | 'unknown'>
Cursor Mode
Gestify.enableCursorMode()
Gestify.disableCursorMode()
Gestify.toggleCursorMode()
Gestify.isCursorModeActive() // → booleanFramework Examples
React
// components/GestureInit.jsx
import { useEffect } from 'react';
export default function GestureInit({ profile = 'default' }) {
useEffect(() => {
import('@jashwanthj/gestify').then(({ default: Gestify }) => Gestify.init({ profile }));
return () => import('@jashwanthj/gestify').then(({ default: Gestify }) => Gestify.destroy());
}, []);
return null;
}
// App.jsx
import GestureInit from './GestureInit';
export default function App() {
return <>
<GestureInit />
{/* rest of your app */}
</>;
}Next.js
// components/GestureInit.jsx
'use client';
import { useEffect } from 'react';
export default function GestureInit({ profile = 'default' }) {
useEffect(() => {
import('@jashwanthj/gestify').then(({ default: Gestify }) => Gestify.init({ profile }));
return () => import('@jashwanthj/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 /></>;
}Vue 3
<!-- 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('@jashwanthj/gestify');
Gestify.init({ profile: props.profile });
});
onUnmounted(async () => {
const { default: Gestify } = await import('@jashwanthj/gestify');
Gestify.destroy();
});
</script>App.vue:
<template>
<GestureInit />
<RouterView />
</template>
<script setup>
import GestureInit from '@/components/GestureInit.vue';
</script>Angular
// 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('@jashwanthj/gestify').then(({ default: Gestify }) => {
this.sdk = Gestify;
Gestify.init({ profile: this.profile });
});
}
ngOnDestroy() { this.sdk?.destroy(); }
}TypeScript
import Gestify from '@jashwanthj/gestify';
import type { GestifyInitOptions } from '@jashwanthj/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
});Events
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 Support
| 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.
Privacy
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.
Troubleshooting
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('@jashwanthj/gestify').then(({ default: Gestify }) => Gestify.init());
// ❌ incorrect — will break SSR
import Gestify from '@jashwanthj/gestify';Content Security Policy (CSP)
Gestify loads MediaPipe from jsDelivr. Allow it:
Content-Security-Policy: script-src 'self' https://cdn.jsdelivr.net;Contributing
Pull requests are welcome. See CONTRIBUTING.md to get started.
Changelog
See CHANGELOG.md.
License
MIT © Gestify Contributors
