visibility-listener
A lightweight, cross-browser library for tracking document visibility state changes with zero dependencies.
Why visibility-listener?
Modern web applications need to know when users are actively viewing a page. Whether you're building analytics, video players, real-time dashboards, or auto-pause features, visibility-listener provides a simple, reliable API to track page visibility across all browsers.
Key Features
โจ Zero Dependencies - Lightweight and fast
๐ Cross-Browser Compatible - Works on modern and legacy browsers (including IE)
๐ฆ Tiny Bundle Size - Minimal impact on your application
๐ฏ TypeScript Support - Fully typed for better developer experience
๐งน Memory Safe - Proper cleanup to prevent memory leaks
๐ Framework Agnostic - Works with React, Vue, Angular, or vanilla JS
Installation
npm install visibility-listener
Quick Start
import createVisibilityStateListener from 'visibility-listener'; const listener = createVisibilityStateListener(); listener.on('update', (state) => { if (state === 'visible') { console.log('User is viewing the page'); } else if (state === 'hidden') { console.log('User switched to another tab'); } }); listener.start();
Real-World Examples
See practical implementations and use cases in our Examples Guide:
- ๐ฌ Auto-Pause Video Player - Save bandwidth by pausing videos when users switch tabs
- ๐ Pause Real-Time Updates - Stop expensive API calls when page is hidden
- ๐ Analytics & User Engagement - Track actual time users spend viewing content
- ๐ฎ Gaming & Animations - Pause game loops to save CPU and battery
- ๐ Notifications & Alerts - Show alerts when users return to your app
- โ๏ธ React Integration - Custom hooks and React patterns
โ View all examples with code
API Reference
Creating a Listener
createVisibilityStateListener(options?)
Creates a new visibility state listener instance.
Parameters:
-
options(optional): Configuration object-
window- Custom window object (useful for testing or iframes) -
document- Custom document object (useful for testing or iframes) -
eventNames.update- Custom event name (default:'update')
-
Returns: VisibilityStateListener instance
Example:
// Basic usage const listener = createVisibilityStateListener(); // With custom event name const listener = createVisibilityStateListener({ eventNames: { update: 'visibilityChanged' } }); // For testing with custom document const listener = createVisibilityStateListener({ window: mockWindow, document: mockDocument });
Instance Methods
listener.on(eventName, callback)
Registers an event listener for visibility changes.
Parameters:
-
eventName(string) - Event name to listen for (default:'update') -
callback(function) - Handler function called with the new visibility state
Returns: void
Example:
listener.on('update', (state) => { console.log('Visibility changed to:', state); // state can be: 'visible', 'hidden', 'prerender', etc. });
listener.start()
Starts listening for visibility changes. Safe to call multiple times.
Returns: boolean - true if started successfully, false if initialization error
Example:
if (listener.start()) { console.log('Listener started successfully'); } else { console.error('Failed to start:', listener.getError()); }
listener.pause()
Pauses the listener. Events won't be emitted, but the listener remains attached.
Returns: boolean - true if paused successfully, false if error
Example:
// Temporarily stop tracking listener.pause(); // Resume tracking listener.start();
listener.destroy()
Completely removes all event listeners and cleans up resources. Always call this when you're done to prevent memory leaks.
Returns: void
Example:
// Cleanup when component unmounts useEffect(() => { const listener = createVisibilityStateListener(); listener.start(); return () => { listener.destroy(); // Important! }; }, []);
listener.getState()
Gets the current visibility state.
Returns: string - Current state ('visible', 'hidden', 'prerender', etc.)
Example:
const currentState = listener.getState(); if (currentState === 'visible') { console.log('Page is currently visible'); }
listener.getLastStateChangeTime()
Gets the timestamp of the most recent visibility change.
Returns: number | null - Timestamp in milliseconds, or null if no changes yet
Example:
const lastChange = listener.getLastStateChangeTime(); if (lastChange) { const timeSinceChange = Date.now() - lastChange; console.log(`Last change was ${timeSinceChange}ms ago`); }
listener.getStateChangeCount()
Gets the total number of visibility changes since the listener started.
Returns: number - Count of state changes
Example:
const changes = listener.getStateChangeCount(); console.log(`User switched tabs ${changes} times`);
listener.hasError()
Checks if there was an initialization error.
Returns: boolean - true if error exists, false otherwise
Example:
if (listener.hasError()) { console.error('Error:', listener.getError()); }
listener.getError()
Gets the error message if initialization failed.
Returns: string | null - Error code or null if no error
Example:
const error = listener.getError(); if (error) { console.error('Initialization failed:', error); }
TypeScript Support
Full TypeScript definitions are included. Import types as needed:
import createVisibilityStateListener, { ErrorCodes } from 'visibility-listener'; import type { VisibilityStateListener, VisibilityStateListenerOptions } from 'visibility-listener'; const listener: VisibilityStateListener = createVisibilityStateListener(); // Type-safe error checking if (listener.getError() === ErrorCodes.INVALID_GLOBALS) { console.error('Window or document not available'); }
Browser Compatibility
| Browser | Support |
|---|---|
| Chrome | โ All versions |
| Firefox | โ All versions |
| Safari | โ All versions |
| Edge | โ All versions |
| IE | โ IE 9+ |
| Mobile Safari | โ All versions |
| Chrome Android | โ All versions |
The library automatically detects and uses the best available API:
- Modern browsers: Page Visibility API
- Older browsers: Focus/blur events
-
Legacy IE:
attachEvent/detachEvent
Advanced Usage
Detecting Visibility States
The listener can detect various visibility states:
-
'visible'- Page is currently visible -
'hidden'- Page is hidden (minimized, background tab, etc.) -
'prerender'- Page is being prerendered (rare)
listener.on('update', (state) => { switch (state) { case 'visible': console.log('User is actively viewing'); break; case 'hidden': console.log('User switched away'); break; case 'prerender': console.log('Page is being prerendered'); break; } });
Multiple Listeners
You can register multiple callbacks for the same event:
const listener = createVisibilityStateListener(); // Multiple handlers are all called listener.on('update', handleAnalytics); listener.on('update', handleVideo); listener.on('update', handlePolling); listener.start();
Conditional Execution
Check state before performing expensive operations:
function updateDashboard() { if (listener.getState() === 'visible') { // Only update if user is watching fetchAndRenderData(); } }
Best Practices
1. Always Clean Up
// โ Bad - memory leak const listener = createVisibilityStateListener(); listener.start(); // โ Good - proper cleanup const listener = createVisibilityStateListener(); listener.start(); // ... later listener.destroy();
2. Check State Before Heavy Operations
// โ Good - save resources function expensiveOperation() { if (listener.getState() === 'hidden') { return; // Don't process if user isn't watching } // ... expensive code }
3. Use with Throttling for Rapid Changes
import { throttle } from 'lodash'; const handleVisibilityChange = throttle((state) => { // Handle state change }, 1000); listener.on('update', handleVisibilityChange);
Troubleshooting
Listener not working?
// Check for errors if (listener.hasError()) { console.error('Error:', listener.getError()); } // Verify it started if (!listener.start()) { console.error('Failed to start listener'); }
Not receiving events?
// Make sure you called start() listener.start(); // Check if paused listener.start(); // Will resume if paused // Verify callback is registered before starting listener.on('update', callback); listener.start(); // Register first, then start
License
This project is licensed under the MIT License.