diff --git a/src/index.ts b/src/index.ts
index 94b995a5..a802eb2b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -77,3 +77,5 @@ export { resolveHookState } from './util/resolveHookState';
// Types
export * from './types';
+
+export * from './useBattery';
diff --git a/src/useBattery/__docs__/example.stories.tsx b/src/useBattery/__docs__/example.stories.tsx
new file mode 100644
index 00000000..d44665c2
--- /dev/null
+++ b/src/useBattery/__docs__/example.stories.tsx
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import { useBattery } from '../..';
+
+export function Example() {
+ const batteryStats = useBattery();
+ return (
+
+
Your battery state:
+
{JSON.stringify(batteryStats, null, 2)}
+
+ );
+}
diff --git a/src/useBattery/__docs__/story.mdx b/src/useBattery/__docs__/story.mdx
new file mode 100644
index 00000000..91daf8c3
--- /dev/null
+++ b/src/useBattery/__docs__/story.mdx
@@ -0,0 +1,45 @@
+import { Canvas, Meta, Story } from '@storybook/addon-docs';
+import { Example } from './example.stories';
+import { ImportPath } from '../../__docs__/ImportPath';
+
+
+
+# useBattery
+
+Hook that tracks battery state.
+
+- Automatically updates on battery state changes.
+- SSR compatible. (Properties return `undefined` on server.)
+
+#### Example
+
+
+
+## Reference
+
+```ts
+export type BatteryState = {
+ /**
+ * @desc {true} if the battery is charging, {false} otherwise.
+ */
+ readonly charging: boolean | undefined;
+ /**
+ * @desc The time remaining in seconds until the system's battery is fully charged.
+ */
+ readonly chargingTime: number | undefined;
+ /**
+ * @desc The time remaining in seconds until the system's battery is fully discharged.
+ */
+ readonly dischargingTime: number | undefined;
+ /**
+ * @desc The battery level of the system as a number between 0 and 1.
+ */
+ readonly level: number | undefined;
+};
+```
+
+#### Importing
+
+
diff --git a/src/useBattery/__tests__/dom.ts b/src/useBattery/__tests__/dom.ts
new file mode 100644
index 00000000..6328fadc
--- /dev/null
+++ b/src/useBattery/__tests__/dom.ts
@@ -0,0 +1,24 @@
+import { renderHook } from '@testing-library/react-hooks/dom';
+import { useBattery } from '../..';
+
+describe('useBattery', () => {
+ it('should be defined', () => {
+ expect(useBattery).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = renderHook(() => useBattery());
+ expect(result.error).toBeUndefined();
+ });
+ it('should return an object of certain structure', () => {
+ const hook = renderHook(() => useBattery(), { initialProps: false });
+
+ expect(typeof hook.result.current).toEqual('object');
+ expect(Object.keys(hook.result.current)).toEqual([
+ 'charging',
+ 'chargingTime',
+ 'dischargingTime',
+ 'level',
+ ]);
+ });
+});
diff --git a/src/useBattery/__tests__/ssr.ts b/src/useBattery/__tests__/ssr.ts
new file mode 100644
index 00000000..6731d105
--- /dev/null
+++ b/src/useBattery/__tests__/ssr.ts
@@ -0,0 +1,32 @@
+import { renderHook } from '@testing-library/react-hooks/server';
+import { useBattery } from '../..';
+
+describe('useBattery', () => {
+ it('should be defined', () => {
+ expect(useBattery).toBeDefined();
+ });
+
+ it('should render', () => {
+ const { result } = renderHook(() => useBattery());
+ expect(result.error).toBeUndefined();
+ });
+ it('should return an object of certain structure', () => {
+ const hook = renderHook(() => useBattery(), { initialProps: false });
+
+ expect(typeof hook.result.current).toEqual('object');
+ expect(Object.keys(hook.result.current)).toEqual([
+ 'charging',
+ 'chargingTime',
+ 'dischargingTime',
+ 'level',
+ ]);
+ });
+ it('should return undefined values', () => {
+ const hook = renderHook(() => useBattery(), { initialProps: false });
+
+ expect(hook.result.current.charging).toBeUndefined();
+ expect(hook.result.current.chargingTime).toBeUndefined();
+ expect(hook.result.current.dischargingTime).toBeUndefined();
+ expect(hook.result.current.level).toBeUndefined();
+ });
+});
diff --git a/src/useBattery/index.ts b/src/useBattery/index.ts
new file mode 100644
index 00000000..13543d77
--- /dev/null
+++ b/src/useBattery/index.ts
@@ -0,0 +1,120 @@
+import { isEqual } from '@react-hookz/deep-equal';
+import { isBrowser } from '../util/const';
+import { off, on } from '../util/misc';
+import { useEffect, useState } from 'react';
+
+/**
+ * The BatteryState interface is the return type of the useBattery Hook.
+ *
+ * provides information about the system's battery charge level
+ * and whether the device is charging, discharging, or fully charged.
+ *
+ * Uses the [BatteryManager](https://developer.mozilla.org/en-US/docs/Web/API/BatteryManager) interface.
+ *
+ * In server-side rendering (SSR) environments, the returned values will be undefined.
+ */
+export type BatteryState = {
+ /**
+ * @desc {true} if the battery is charging, {false} otherwise.
+ */
+ readonly charging: boolean | undefined;
+ /**
+ * @desc The time remaining in seconds until the system's battery is fully charged.
+ */
+ readonly chargingTime: number | undefined;
+ /**
+ * @desc The time remaining in seconds until the system's battery is fully discharged.
+ */
+ readonly dischargingTime: number | undefined;
+ /**
+ * @desc The battery level of the system as a number between 0 and 1.
+ */
+ readonly level: number | undefined;
+};
+
+type BatteryManager = {
+ readonly charging: boolean;
+ readonly chargingTime: number;
+ readonly dischargingTime: number;
+ readonly level: number;
+} & EventTarget;
+
+type NavigatorWithPossibleBattery = Navigator & {
+ getBattery?: () => Promise;
+};
+
+const nav: NavigatorWithPossibleBattery | undefined = isBrowser ? navigator : undefined;
+/**
+ * Hook that tracks battery state.
+ *
+ * @see https://react-hookz.github.io/web/?path=/docs/navigator-usebattery
+ *
+ * @returns
+ * - {charging} - whether the system is charging
+ * - {chargingTime} - time remaining in seconds until the system is fully charged
+ * - {dischargingTime} - time remaining in seconds until the system is fully discharged
+ * - {level} - battery level in percent
+ *
+ * In server-side rendering (SSR) environments, the returned values will be undefined.
+ */
+export function useBattery(): BatteryState {
+ const [state, setState] = useState({
+ charging: undefined,
+ chargingTime: undefined,
+ dischargingTime: undefined,
+ level: undefined,
+ });
+
+ useEffect(() => {
+ let isMounted = true;
+ let battery: BatteryManager | null = null;
+
+ const handleChange = () => {
+ if (!isMounted || !battery) {
+ return;
+ }
+
+ const newState: BatteryState = {
+ level: battery.level,
+ charging: battery.charging,
+ dischargingTime: battery.dischargingTime,
+ chargingTime: battery.chargingTime,
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-expressions
+ !isEqual(state, newState) && setState(newState);
+ };
+
+ nav
+ ?.getBattery?.()
+ .then((bat: BatteryManager) => {
+ // eslint-disable-next-line promise/always-return
+ if (!isMounted) {
+ return;
+ }
+
+ battery = bat;
+ on(battery, 'chargingchange', handleChange);
+ on(battery, 'chargingtimechange', handleChange);
+ on(battery, 'dischargingtimechange', handleChange);
+ on(battery, 'levelchange', handleChange);
+ handleChange();
+ })
+ .catch(() => {
+ // ignore
+ });
+
+ return () => {
+ isMounted = false;
+ if (battery) {
+ off(battery, 'chargingchange', handleChange);
+ off(battery, 'chargingtimechange', handleChange);
+ off(battery, 'dischargingtimechange', handleChange);
+ off(battery, 'levelchange', handleChange);
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return state;
+}