visibility-listener

6 min read Original article โ†—

visibility-listener

npm version GitHub license

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:

โ†’ 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.