diff --git a/docs/useDocumentVisibility.md b/docs/useDocumentVisibility.md
new file mode 100644
index 0000000000..4ffb1a16bc
--- /dev/null
+++ b/docs/useDocumentVisibility.md
@@ -0,0 +1,29 @@
+# `useDocumentVisibility`
+
+React sensor hook that tracks document visibility state using the [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API).
+
+## Usage
+
+```jsx
+import {useDocumentVisibility} from 'react-use';
+
+const Demo = () => {
+ const defaultState = document.visibilityState === 'visible';
+ const isVisible = useDocumentVisibility(defaultState);
+
+ return (
+
+ Document is {isVisible ? 'visible' : 'hidden'}
+
+ );
+};
+```
+
+## Reference
+
+```js
+const isVisible = useDocumentVisibility(initialState);
+```
+
+- `initialState` — `boolean`, optional initial state before the actual visibility is determined, defaults to `false`.
+- `isVisible` — `boolean`, whether the document is currently visible (tab is in foreground).
diff --git a/src/index.ts b/src/index.ts
index 62b69356b7..42981a6394 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -18,6 +18,7 @@ export { default as useCustomCompareEffect } from './useCustomCompareEffect';
export { default as useDebounce } from './useDebounce';
export { default as useDeepCompareEffect } from './useDeepCompareEffect';
export { default as useDefault } from './useDefault';
+export { default as useDocumentVisibility } from './useDocumentVisibility';
export { default as useDrop } from './useDrop';
export { default as useDropArea } from './useDropArea';
export { default as useEffectOnce } from './useEffectOnce';
diff --git a/src/useDocumentVisibility.ts b/src/useDocumentVisibility.ts
new file mode 100644
index 0000000000..1f7b7f1ae8
--- /dev/null
+++ b/src/useDocumentVisibility.ts
@@ -0,0 +1,19 @@
+import { useEffect, useState } from 'react';
+
+const useDocumentVisibility = (defaultState: boolean = false) => {
+ const [isVisible, setIsVisible] = useState(defaultState);
+
+ useEffect(() => {
+ const handleVisibilityChange = () => setIsVisible(document.visibilityState === 'visible');
+
+ document.addEventListener('visibilitychange', handleVisibilityChange);
+
+ return () => {
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
+ };
+ }, []);
+
+ return isVisible;
+};
+
+export default useDocumentVisibility;
diff --git a/stories/useDocumentVisibility.story.tsx b/stories/useDocumentVisibility.story.tsx
new file mode 100644
index 0000000000..c97396c565
--- /dev/null
+++ b/stories/useDocumentVisibility.story.tsx
@@ -0,0 +1,22 @@
+import { storiesOf } from '@storybook/react';
+import * as React from 'react';
+import useDocumentVisibility from '../src/useDocumentVisibility';
+import ShowDocs from './util/ShowDocs';
+
+const Demo = () => {
+ const defaultState = document.visibilityState === 'visible';
+ const isVisible = useDocumentVisibility(defaultState);
+
+ return (
+
+
Switch to another browser tab to see the visibility state change.
+
+ Document is {isVisible ? '👁️ Visible' : '🙈 Hidden'}
+
+
+ );
+};
+
+storiesOf('Sensors/useDocumentVisibility', module)
+ .add('Docs', () => )
+ .add('Demo', () => );
diff --git a/tests/useDocumentVisibility.test.ts b/tests/useDocumentVisibility.test.ts
new file mode 100644
index 0000000000..5ddc0ed09d
--- /dev/null
+++ b/tests/useDocumentVisibility.test.ts
@@ -0,0 +1,92 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+import useDocumentVisibility from '../src/useDocumentVisibility';
+
+describe('useDocumentVisibility', () => {
+ const originalVisibilityState = document.visibilityState;
+
+ afterEach(() => {
+ Object.defineProperty(document, 'visibilityState', {
+ configurable: true,
+ value: originalVisibilityState,
+ });
+ });
+
+ it('should be defined', () => {
+ expect(useDocumentVisibility).toBeDefined();
+ });
+
+ it('should return false initially', () => {
+ const { result } = renderHook(() => useDocumentVisibility());
+
+ expect(result.current).toBe(false);
+ });
+
+ it('should return true initially when initialState is true', () => {
+ const { result } = renderHook(() => useDocumentVisibility(true));
+
+ expect(result.current).toBe(true);
+ });
+
+ it('should return false initially when initialState is false', () => {
+ const { result } = renderHook(() => useDocumentVisibility(false));
+
+ expect(result.current).toBe(false);
+ });
+
+ it('should return true when document becomes visible', () => {
+ const { result } = renderHook(() => useDocumentVisibility(true));
+
+ act(() => {
+ Object.defineProperty(document, 'visibilityState', {
+ configurable: true,
+ value: 'visible',
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ expect(result.current).toBe(true);
+ });
+
+ it('should return false when document becomes hidden', () => {
+ const { result } = renderHook(() => useDocumentVisibility());
+
+ act(() => {
+ Object.defineProperty(document, 'visibilityState', {
+ configurable: true,
+ value: 'visible',
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+ expect(result.current).toBe(true);
+
+ act(() => {
+ Object.defineProperty(document, 'visibilityState', {
+ configurable: true,
+ value: 'hidden',
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+ expect(result.current).toBe(false);
+ });
+
+ it('should add event listener on mount', () => {
+ const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
+
+ renderHook(() => useDocumentVisibility());
+
+ expect(addEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
+
+ addEventListenerSpy.mockRestore();
+ });
+
+ it('should remove event listener on unmount', () => {
+ const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
+
+ const { unmount } = renderHook(() => useDocumentVisibility());
+ unmount();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
+
+ removeEventListenerSpy.mockRestore();
+ });
+});