useIdle
Detect when the user's system is completely idle (away from keyboard) using the IdleDetector API. Great for auto-lock/logout features.
Installation
npx shadcn@latest add https://r.fiberui.com/r/hooks/use-idle.jsonA React hook that implements the Idle Detector API, allowing you to find out when the user is completely idle, such as being away from their device. This goes beyond simple DOM events by detecting system-level inactivity.
Experimental API
The Idle Detector API is currently experimental and requires explicit
permission from the user. Check isSupported before requesting permission.
Features
- System-level Detection - Detects if the user is away from their device entirely
- Screen State - Can detect if the screen is locked
- Type Safe - Fully typed for TypeScript
- Permission Management - Built-in helper for requesting API permissions
Basic Usage
System Idle Detection
System Idle Detection
Detects if the user is away from their keyboard entirely (1 min).
"use client";
import { useIdle } from "@repo/hooks/performance/use-idle";
import { Button } from "@repo/ui/components/button";
import { Coffee, MonitorPlay } from "lucide-react";
export function Example1() {
const { idle, isSupported, isGranted, requestPermission } = useIdle();
return (
<div className="mx-auto flex w-full max-w-sm flex-col items-center justify-center gap-6">
<div className="text-center">
<h3 className="text-lg font-medium">System Idle Detection</h3>
<p className="text-muted-foreground text-sm">
Detects if the user is away from their keyboard entirely (1
min).
</p>
</div>
<div
className={`flex items-center justify-center rounded-full border-4 p-8 transition-all duration-700 ${
idle
? "border-amber-500 bg-amber-500/10 shadow-[0_0_30px_rgba(245,158,11,0.3)] dark:bg-amber-500/20"
: "border-primary/50 bg-primary/10"
}`}
>
{idle ? (
<Coffee className="h-16 w-16 animate-pulse text-amber-500" />
) : (
<MonitorPlay className="text-primary h-16 w-16" />
)}
</div>
<div className="text-xl font-bold tracking-tight">
Status:{" "}
{idle ? (
<span className="text-amber-500">Away (Idle)</span>
) : (
<span className="text-primary">Active</span>
)}
</div>
{!isSupported && (
<div className="rounded-md bg-red-100 p-3 text-center text-sm text-red-600 dark:bg-red-900/30 dark:text-red-400">
Idle Detection is not supported in this browser. Try
Chrome/Edge.
</div>
)}
{isSupported && !isGranted && (
<div className="flex w-full flex-col gap-3">
<p className="text-muted-foreground text-center text-sm">
This feature requires explicit permission.
</p>
<Button onClick={requestPermission} className="w-full">
Enable Idle Detection
</Button>
</div>
)}
</div>
);
}
Auto-Lock Session
Banking Dashboard
Available Balance
$42,450.00
"use client";
import { useIdle } from "@repo/hooks/performance/use-idle";
import { useState, useEffect } from "react";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { Lock, Unlock } from "lucide-react";
export function Example2() {
const { idle, isGranted, requestPermission } = useIdle();
const [isLocked, setIsLocked] = useState(false);
useEffect(() => {
// Automatically lock when idle changes to true
if (idle && isGranted) {
setIsLocked(true);
}
}, [idle, isGranted]);
return (
<Card className="relative mx-auto w-full max-w-sm overflow-hidden p-6">
<div
className={`transition-all duration-500 ${isLocked ? "pointer-events-none select-none opacity-50 blur-md" : ""}`}
>
<div className="mb-6 flex items-center justify-between">
<h3 className="font-bold">Banking Dashboard</h3>
<div className="rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700 dark:bg-green-900 dark:text-green-400">
Secure Session
</div>
</div>
<div className="space-y-4">
<div>
<p className="text-muted-foreground text-xs uppercase tracking-wider">
Available Balance
</p>
<p className="text-3xl font-black">$42,450.00</p>
</div>
<div className="mt-6 space-y-2">
<div className="flex items-center justify-between border-b py-2">
<span className="text-sm">Apple Store</span>
<span className="text-destructive text-sm font-medium">
-$1,299.00
</span>
</div>
<div className="flex items-center justify-between border-b py-2">
<span className="text-sm">Salary Deposit</span>
<span className="text-sm font-medium text-green-600 dark:text-green-400">
+$4,200.00
</span>
</div>
</div>
</div>
{!isGranted && (
<Button
variant="outline"
size="sm"
onClick={requestPermission}
className="mt-6 w-full"
>
Enable Auto-Lock Feature
</Button>
)}
</div>
{/* Lock Overlay */}
{isLocked && (
<div className="bg-background/50 animate-in fade-in zoom-in-95 absolute inset-0 z-10 flex flex-col items-center justify-center p-6 text-center backdrop-blur-sm">
<div className="bg-primary/20 text-primary mb-4 flex h-16 w-16 items-center justify-center rounded-full">
<Lock className="h-8 w-8" />
</div>
<h4 className="mb-2 text-xl font-bold">Session Locked</h4>
<p className="text-muted-foreground mb-6 text-sm">
Your session was automatically locked because you were
away from your device.
</p>
<Button
onClick={() => setIsLocked(false)}
className="w-full gap-2"
>
<Unlock className="h-4 w-4" />
Unlock Session
</Button>
</div>
)}
</Card>
);
}
Privacy Protection
Privacy Protection
Hides sensitive content when you walk away.
Confidential Q3 Strategy
Content Hidden
Document protected while you are away from the system.
"use client";
import { useIdle } from "@repo/hooks/performance/use-idle";
import { useState, useEffect } from "react";
import { Button } from "@repo/ui/components/button";
import { FileText, EyeOff } from "lucide-react";
export function Example3() {
const { idle, isGranted, requestPermission } = useIdle();
const [blurEnabled, setBlurEnabled] = useState(true);
const isCurrentlyHidden = idle && blurEnabled && isGranted;
return (
<div className="mx-auto flex w-full max-w-lg flex-col gap-6">
<div className="bg-muted/50 flex flex-col items-center justify-between gap-4 rounded-lg p-4 sm:flex-row">
<div className="space-y-1 text-center sm:text-left">
<h3 className="text-sm font-bold">Privacy Protection</h3>
<p className="text-muted-foreground text-xs">
Hides sensitive content when you walk away.
</p>
</div>
<div className="flex gap-2">
{!isGranted && (
<Button
size="sm"
variant="outline"
onClick={requestPermission}
>
Allow Sensor
</Button>
)}
<Button
size="sm"
variant={blurEnabled ? "default" : "secondary"}
onClick={() => setBlurEnabled(!blurEnabled)}
isDisabled={!isGranted}
>
{blurEnabled ? "Protection ON" : "Protection OFF"}
</Button>
</div>
</div>
<div className="bg-background relative overflow-hidden rounded-xl border">
{/* Simulated Document Content */}
<div
className={`p-8 transition-all duration-1000 ${isCurrentlyHidden ? "scale-95 opacity-20 blur-xl grayscale" : ""}`}
>
<div className="text-primary mb-6 flex items-center gap-2">
<FileText className="h-5 w-5" />
<h2 className="font-bold">Confidential Q3 Strategy</h2>
</div>
<div className="space-y-4">
<div className="bg-muted h-4 w-3/4 rounded"></div>
<div className="bg-muted h-4 w-full rounded"></div>
<div className="bg-muted h-4 w-5/6 rounded"></div>
<div className="bg-primary/5 border-primary/20 !mb-6 mt-6 h-32 w-full rounded border"></div>
<div className="bg-muted h-4 w-2/3 rounded"></div>
<div className="bg-muted h-4 w-full rounded"></div>
</div>
</div>
{/* Privacy Overlay */}
<div
className={`pointer-events-none absolute inset-0 flex flex-col items-center justify-center p-6 text-center transition-all duration-500 ${
isCurrentlyHidden
? "bg-background/40 opacity-100"
: "opacity-0"
}`}
>
<EyeOff className="text-muted-foreground mb-4 h-12 w-12" />
<h4 className="text-foreground text-lg font-bold">
Content Hidden
</h4>
<p className="text-foreground/80 mt-1 max-w-xs text-sm">
Document protected while you are away from the system.
</p>
</div>
</div>
</div>
);
}
Smart Time Tracker
Smart Time Tracker
Automatically pauses billing/tracking when you walk away.
"use client";
import { useIdle } from "@repo/hooks/performance/use-idle";
import { useState, useEffect } from "react";
import { Button } from "@repo/ui/components/button";
import { Card } from "@repo/ui/components/card";
import { Play, Pause, Square } from "lucide-react";
export function Example4() {
const { idle, isGranted, requestPermission } = useIdle();
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
const [wasPausedByIdle, setWasPausedByIdle] = useState(false);
// Timer logic
useEffect(() => {
let interval: NodeJS.Timeout;
if (isRunning) {
interval = setInterval(() => {
setSeconds((s) => s + 1);
}, 1000);
}
return () => clearInterval(interval);
}, [isRunning]);
// Idle Pause Logic
useEffect(() => {
if (!isGranted) return;
if (idle && isRunning) {
// User went idle while timer was running -> Auto pause
setIsRunning(false);
setWasPausedByIdle(true);
} else if (!idle && !isRunning && wasPausedByIdle) {
// User came back and it was paused by the idle detector -> Auto resume
setIsRunning(true);
setWasPausedByIdle(false);
}
}, [idle, isGranted, isRunning, wasPausedByIdle]);
const formatTime = (secs: number) => {
const m = Math.floor(secs / 60)
.toString()
.padStart(2, "0");
const s = (secs % 60).toString().padStart(2, "0");
return `${m}:${s}`;
};
return (
<Card className="mx-auto flex max-w-sm flex-col items-center justify-center gap-6 p-6">
<div className="relative w-full text-center">
<h3 className="text-lg font-medium">Smart Time Tracker</h3>
<p className="text-muted-foreground mt-1 text-sm">
Automatically pauses billing/tracking when you walk away.
</p>
{!isGranted && (
<Button
variant="link"
onClick={requestPermission}
className="mt-2 h-auto p-0 text-xs"
>
Enable Auto-Pause
</Button>
)}
</div>
<div className="relative">
<div
className={`text-6xl font-black tabular-nums tracking-tighter transition-colors ${
idle
? "opacity-50"
: isRunning
? "text-primary"
: "text-muted-foreground"
}`}
>
{formatTime(seconds)}
</div>
{idle && wasPausedByIdle && (
<div className="absolute -bottom-6 left-1/2 -translate-x-1/2 animate-pulse whitespace-nowrap rounded-full bg-amber-500/10 px-2 py-0.5 text-xs font-bold text-amber-500">
Auto-Paused (Away)
</div>
)}
</div>
<div className="mt-4 flex w-full gap-2">
<Button
className="flex-1 gap-2"
variant={isRunning ? "outline" : "default"}
onClick={() => {
setIsRunning(!isRunning);
setWasPausedByIdle(false); // Reset auto-pause tracking on manual action
}}
>
{isRunning ? (
<>
<Pause className="h-4 w-4" /> Pause
</>
) : (
<>
<Play className="h-4 w-4" /> Start
</>
)}
</Button>
<Button
variant="destructive"
size="icon"
onClick={() => {
setIsRunning(false);
setSeconds(0);
setWasPausedByIdle(false);
}}
>
<Square className="h-4 w-4" />
</Button>
</div>
</Card>
);
}
API Reference
Hook Signature
function useIdle(): UseIdleReturn;Return Value
| Property | Type | Description |
|---|---|---|
idle | boolean | true if the device is idle or the screen is locked |
isSupported | boolean | true if the Idle Detector API is available |
isGranted | boolean | true if permission has been granted by the user |
requestPermission | () => Promise<boolean> | Helper to prompt the user for permission to use it |
Hook Source Code
import { useState, useEffect, useCallback, useRef } from "react";
interface IdleState {
idle: boolean;
isSupported: boolean;
isGranted: boolean;
}
/**
* Detect when the user's system is completely idle using the IdleDetector API.
*/
export function useIdle() {
const [state, setState] = useState<IdleState>({
idle: false,
isSupported: false,
isGranted: false,
});
const isSupported =
typeof window !== "undefined" && "IdleDetector" in window;
// We can only use the IdleDetector interface if supported.
// The IdleDetector needs permission before using it.
const requestPermission = useCallback(async () => {
if (!isSupported) return false;
try {
// @ts-ignore - IdleDetector is an experimental API
const status = await IdleDetector.requestPermission();
setState((prev) => ({ ...prev, isGranted: status === "granted" }));
return status === "granted";
} catch (error) {
console.error("Failed to request IdleDetector permission:", error);
return false;
}
}, [isSupported]);
useEffect(() => {
setState((prev) => ({ ...prev, isSupported }));
if (!isSupported) return;
// Check if permission was already granted
// @ts-ignore
navigator.permissions
.query({ name: "idle-detection" as PermissionName })
.then((status) => {
if (status.state === "granted") {
setState((prev) => ({ ...prev, isGranted: true }));
}
});
}, [isSupported]);
useEffect(() => {
if (!state.isGranted) return;
let detector: any;
const abortController = new AbortController();
const startIdleDetection = async () => {
try {
// @ts-ignore
detector = new IdleDetector();
detector.addEventListener("change", () => {
const isIdle =
detector.userState === "idle" ||
detector.screenState === "locked";
setState((prev) => ({ ...prev, idle: isIdle }));
});
await detector.start({
threshold: 60000,
signal: abortController.signal,
});
} catch (err) {
console.error("Failed to start IdleDetector:", err);
}
};
startIdleDetection();
return () => {
abortController.abort();
};
}, [state.isGranted]);
return { ...state, requestPermission };
}