Modern web applications increasingly rely on real-time state synchronization, live collaboration, and background processing. However, keeping heavy tasks—such as WebSocket heartbeat polling, high-frequency chart rendering, or video canvas re-paints—active while a user is away from their keyboard drains battery and wastes system resources.
The Idle Detection API provides a native browser interface to detect when a user is inactive or when their screen locks. Instead of relying on fragile mouse-movement timers or continuous event listeners, developers can subscribe to native operational state changes directly from the underlying OS.
Key Requirements & Security Prerequisites
Before deploying the Idle Detection API to production, ensure your application satisfies the following browser policies:
- Secure Context (HTTPS): The API is restricted to secure origins (
https://). - Explicit Permission (
idle-detection): The browser requires user authorization viaIdleDetector.requestPermission(). - Transient Activation: Permission requests must be triggered by a user gesture (e.g., a button click).
- Feature-Policy / Permissions Policy: If loaded inside an iframe, the document must explicitly allow
idle-detection.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Idle Detection API Demo</title>
<link href="style.css" rel="stylesheet" />
<script src="idleDetection_API.js" defer></script>
</head>
<body>
<main class="container">
<header class="header">
<h1>Idle Detection Monitor</h1>
<p class="subtitle">Track real-time user and screen idle transitions.</p>
</header>
<!-- Trigger button to request user consent -->
<section class="controls">
<button id="start-btn" class="btn">Start Monitoring</button>
<span id="permission-status" class="status-badge">Permission: Not Requested</span>
</section>
<!-- Indicator lights showing current state -->
<section class="state-indicators">
<div class="indicator-card">
<span class="label">User State</span>
<span id="user-state" class="state-value">Unknown</span>
</div>
<div class="indicator-card">
<span class="label">Screen State</span>
<span id="screen-state" class="state-value">Unknown</span>
</div>
</section>
<!-- Live event log window -->
<section class="log-section">
<h2>Activity Log</h2>
<ul id="log-list" class="log-list">
<li class="log-item info">Click "Start Monitoring" to request permission.</li>
</ul>
</section>
</main>
</body>
</html>
/* Base Layout & Responsive Styling */
:root {
--bg-color: #0f172a;
--card-bg: #1e293b;
--text-main: #f8fafc;
--text-muted: #94a3b8;
--accent-color: #2563eb;
--accent-hover: #1d4ed8;
--border-color: #334155;
--active-color: #22c55e;
--idle-color: #eab308;
--locked-color: #ef4444;
}
body {
margin: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 1.5rem;
box-sizing: border-box;
}
.container {
width: 100%;
max-width: 680px;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
}
.header h1 {
margin: 0 0 0.5rem 0;
font-size: 1.75rem;
font-weight: 700;
}
.subtitle {
margin: 0 0 1.5rem 0;
color: var(--text-muted);
font-size: 0.95rem;
}
/* Control Panel */
.controls {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.btn {
background-color: var(--accent-color);
color: #fff;
border: none;
padding: 0.75rem 1.25rem;
font-size: 0.95rem;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn:hover {
background-color: var(--accent-hover);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status-badge {
font-size: 0.85rem;
color: var(--text-muted);
background: var(--bg-color);
padding: 0.4rem 0.75rem;
border-radius: 4px;
border: 1px solid var(--border-color);
}
/* State Indicators */
.state-indicators {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-bottom: 1.5rem;
}
.indicator-card {
background-color: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.label {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
.state-value {
font-size: 1.25rem;
font-weight: 700;
color: var(--text-main);
}
/* Activity Log */
.log-section h2 {
font-size: 1.1rem;
margin: 0 0 0.75rem 0;
color: var(--text-muted);
}
.log-list {
list-style: none;
padding: 0;
margin: 0;
max-height: 220px;
overflow-y: auto;
border: 1px solid var(--border-color);
border-radius: 6px;
background-color: var(--bg-color);
font-family: monospace;
font-size: 0.85rem;
}
.log-item {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border-color);
color: var(--text-main);
}
.log-item:last-child {
border-bottom: none;
}
.log-item.info {
color: var(--text-muted);
}
.log-item.active {
color: var(--active-color);
}
.log-item.idle {
color: var(--idle-color);
}
.log-item.locked {
color: var(--locked-color);
}
// Store references to DOM elements using document.querySelector
const startBtn = document.querySelector('#start-btn');
const permissionStatus = document.querySelector('#permission-status');
const userStateEl = document.querySelector('#user-state');
const screenStateEl = document.querySelector('#screen-state');
const logList = document.querySelector('#log-list');
// Define minimum threshold for idle state triggering (in milliseconds; min 60,000 ms per spec)
const IDLE_THRESHOLD_MS = 60000;
// Helper function to append timestamped messages to the UI log list
const addLogEntry = (message, type = 'info') => {
const timestamp = new Date().toLocaleTimeString();
const li = document.createElement('li');
li.className = `log-item ${type}`;
li.textContent = `[${timestamp}] ${message}`;
// Prepend to show the latest state changes at the top
logList.prepend(li);
};
// Update status text in the UI indicator cards
const updateIndicators = (userState, screenState) => {
userStateEl.textContent = userState;
screenStateEl.textContent = screenState;
};
// Main function to initialize and start the IdleDetector
const initIdleDetection = async () => {
// 1. Verify browser compatibility
if (!('IdleDetector' in window)) {
addLogEntry(
'Error: Idle Detection API is not supported in this browser.',
'locked'
);
permissionStatus.textContent = 'Status: Unsupported';
return;
}
try {
// 2. Request user permission (must be called from a user gesture)
addLogEntry('Requesting Idle Detection permission...', 'info');
const permission = await IdleDetector.requestPermission();
permissionStatus.textContent = `Permission: ${permission}`;
if (permission !== 'granted') {
addLogEntry('Permission denied by user.', 'locked');
return;
}
// 3. Instantiate the IdleDetector instance
const idleDetector = new IdleDetector();
// 4. Attach event listener for change events using arrow functions
idleDetector.addEventListener('change', () => {
const { userState, screenState } = idleDetector;
// Log state transition
addLogEntry(
`State change — User: ${userState} | Screen: ${screenState}`,
userState === 'active' ? 'active' : 'idle'
);
// Update DOM indicators
updateIndicators(userState, screenState);
// Example operational hooks:
if (userState === 'idle' || screenState === 'locked') {
// Pause resource-intensive processes (e.g., real-time polling, video canvas)
addLogEntry('System paused: entering idle mode.', 'idle');
} else if (userState === 'active') {
// Resume background activity
addLogEntry('System resumed: user returned active.', 'active');
}
});
// 5. Start listening with a 60-second minimum threshold
await idleDetector.start({
threshold: IDLE_THRESHOLD_MS
});
addLogEntry(
`Idle detector active (Threshold: ${IDLE_THRESHOLD_MS / 1000}s).`,
'active'
);
// Set initial values
updateIndicators(idleDetector.userState, idleDetector.screenState);
// Disable start button once initialized
startBtn.disabled = true;
startBtn.textContent = 'Monitoring Active';
} catch (error) {
addLogEntry(`Initialization error: ${error.message}`, 'locked');
}
};
// Attach click listener to start button using arrow function
startBtn.addEventListener('click', () => {
initIdleDetection();
});





