<svg class="inactivity-ring" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="25" cy="25" r="20" stroke-width="10" stroke-linejoin="round" />
</svg>
<main>
<header>
<h1>Inactivity Pop-up</h1>
</header>
<article>
<p>
<strong>
In this demo, leave the page inactive for 4 seconds, and a pop-up will appear. Inactivity pop-ups are often seen in apps that display secure information. They often tell the user they'll get logged out if the inactivity continues.
</strong>
</p>
<p>Lorem ipsum, dolor, sit amet consectetur adipisicing elit. Repellat illo eum hic possimus, libero voluptates, facere nihil deserunt similique error sunt vero minima enim cupiditate quis velit rem ab repellendus.</p>
<p>Neque hic veniam cum voluptates quae magnam quisquam saepe. Dolorum quasi amet officiis vero aliquid pariatur commodi repellendus. Quos iure nobis doloremque ipsum facere nostrum neque adipisci hic labore voluptates?</p>
<p>Mollitia beatae, repellat est laboriosam officiis? Earum veritatis incidunt explicabo id tempora perspiciatis non doloribus natus nisi asperiores quia error autem laudantium, amet doloremque, vitae quos. Soluta unde numquam id.</p>
<p>Recusandae facere esse quod dolorum maxime corrupti ex quam iure id quis eius cumque fugiat, consequatur autem, excepturi molestias libero velit illo a quo. Sed quae aut inventore quia, ipsa.</p>
<p>Eius id laboriosam, quas iure laborum corrupti optio corporis tempore odit laudantium similique, accusamus, vel! Perspiciatis sed saepe rem. Nihil explicabo ea at vitae rerum eum, corrupti ipsam suscipit, id!</p>
<p>Voluptate pariatur, cum praesentium, veniam tenetur distinctio totam voluptates ea beatae, dicta voluptatum illum sit! Earum harum odio natus perspiciatis. Magni eos temporibus harum quas laborum aspernatur, minus, sunt illum.</p>
<p>Ducimus nobis, libero illum commodi aliquid. Molestias beatae ducimus ratione quod, quis itaque ipsum, illum amet labore, nostrum quisquam enim. Sint aperiam qui nihil eaque id quam odio? Repudiandae, sint!</p>
<p>In rem tenetur accusamus accusantium id, officia quaerat atque, placeat architecto nihil unde, sit cupiditate minima tempore! Magni, atque labore. Dolor voluptatum eius incidunt, enim? Asperiores similique, ut amet atque?</p>
<p>Illum, quam fugiat, veniam officia totam nihil aut dolore quo! Officiis ipsa harum vero voluptatibus eaque repudiandae amet eligendi ducimus laboriosam ab maiores labore, odit, voluptas quos illo sint nulla.</p>
<p>Eius earum nihil officiis, inventore accusantium consequuntur dolor reprehenderit in esse quos eos iste nemo amet facere, mollitia, assumenda cum provident quam. Ipsam, vitae! Excepturi quos possimus enim, quia distinctio.</p>
</article>
</main>
<div id="inactivity-pop-up" popover>
<div class="card elevated">
<div class="title">You've barely touched your HTML</div>
<div class="subtitle">It'll self-destruct in <span class="seconds"></span> seconds unless you interact.</div>
<div class="actions">
<button class="button filled ripple" popoverhidetarget="inactivity-pop-up">Keep it</button>
<button class="button destructive ripple">Destroy it</button>
</div>
</div>
</div>
@layer demo {
[popover] {
transition: transform 0.2s;
transform: scale(var(--open, 0));
max-width: calc(100% - (2 * var(--size-4)));
overflow: visible;
}
[popover]:open,
[popover]:open::backdrop {
--open: 1;
}
[popover]::backdrop {
transition: opacity 0.2s;
opacity: var(--open, 0);
background: hsl(0 0% 10% / 0.5);
}
.seconds {
font-weight: var(--font-weight-9);
}
.card {
min-width: 0;
}
.card .actions {
justify-content: flex-end;
gap: 1ch;
}
.inactivity-ring {
fill: none;
position: fixed;
top: var(--size-4);
right: var(--size-4);
color: var(--red-6);
width: 40px;
aspect-ratio: 1;
stroke-dasharray: 130;
stroke-dashoffset: 130;
transform: rotate(-90deg);
z-index: 20;
}
.inactivity-ring circle {
stroke: currentColor;
}
.timing .inactivity-ring {
animation: load calc(var(--threshold) * 1ms) linear;
}
.button.destructive {
background: var(--md-sys-color-error);
color: var(--md-sys-color-surface);
}
.button.destructive:is(:hover, :focus) {
background: var(--red-6);
}
@keyframes load {
to {
stroke-dashoffset: 0;
}
}
}
@layer base {
*,
*:after,
*:before {
box-sizing: border-box;
/*cursor: none;*/
touch-action: none;
}
body {
display: block;
min-height: 100vh;
font-family: "Google Sans", sans-serif, system-ui;
overflow: auto;
}
:where([popover]) {
margin: auto;
border-width: 0;
border-style: none;
background: transparent;
}
h1 {
margin: 0;
color: var(--gray-0);
}
header {
height: 25vmin;
min-height: 200px;
background: var(--gradient-2);
display: grid;
place-items: center;
color: var(--gray-0);
padding: var(--size-4);
grid-template-columns: 1fr;
}
main {
margin: 0 auto;
}
article {
padding: var(--size-4);
display: flex;
flex-direction: column;
align-items: center;
}
article > * + * {
margin-top: var(--size-4);
}
}
const POPUP = document.querySelector("#inactivity-pop-up");
const RING = document.querySelector("svg");
const BUTTON = document.querySelector("button.destructive");
const SCREENSAVER_THRESHOLD = 4000;
const DESTRUCTIVE_THRESHOLD = 9;
let checker;
let screensaverTimeout;
let secondsInterval;
const EVENT_TYPES = [
"pointermove",
"keypress",
"keydown",
"keyup",
"scroll",
"click"
];
const setSaverTimer = () => {
if (screensaverTimeout) {
clearTimeout(screensaverTimeout);
document.body.classList.remove("timing");
}
if (!POPUP.matches(":open")) {
screensaverTimeout = setTimeout(() => {
document.body.classList.remove("timing");
POPUP.showPopover();
}, SCREENSAVER_THRESHOLD);
requestAnimationFrame(() => {
document.body.classList.add("timing");
});
}
};
const destroy = () => {
if (secondsInterval) clearInterval(secondsInterval);
document.body.style = `
display: grid;
place-items: center;
`;
document.body.innerHTML = "<p>it's gone...</p>";
if (screensaverTimeout) {
clearTimeout(screensaverTimeout);
document.body.classList.remove("timing");
}
EVENT_TYPES.forEach((e) =>
document.body.removeEventListener(e, setSaverTimer)
);
};
document.body.classList.add("timing");
document.documentElement.style.setProperty(
"--threshold",
SCREENSAVER_THRESHOLD
);
const SECONDS = document.querySelector(".seconds");
POPUP.addEventListener("beforetoggle", ({ newState }) => {
if (newState === "closed") {
if (POPUP.parentNode) setSaverTimer();
if (secondsInterval) clearInterval(secondsInterval);
} else {
SECONDS.innerText = DESTRUCTIVE_THRESHOLD;
secondsInterval = setInterval(() => {
SECONDS.innerText -= 1;
if (parseInt(SECONDS.innerText, 10) === 0) destroy();
}, 1000);
}
});
BUTTON.addEventListener("click", destroy);
setSaverTimer();
EVENT_TYPES.forEach((e) => document.body.addEventListener(e, setSaverTimer));