Pages Easy
Offline Page
A PWA offline fallback page with network detection, automatic reconnect polling, and a cached content list. Pure CSS + minimal JS.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f9fafb;
min-height: 100vh;
color: #111;
display: flex;
align-items: center;
justify-content: center;
}
.page {
width: 100%;
max-width: 440px;
padding: 40px 24px 60px;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
text-align: center;
}
.logo {
font-size: 20px;
font-weight: 800;
color: #111;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
}
.wifi-icon svg {
width: 80px;
height: 80px;
}
h1 {
font-size: 28px;
font-weight: 800;
color: #111;
}
p {
font-size: 15px;
color: #666;
line-height: 1.6;
max-width: 340px;
}
/* Status row */
.status-row {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #fff;
border-radius: 24px;
border: 1px solid #e5e7eb;
font-size: 13px;
font-weight: 500;
color: #555;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.offline {
background: #ef4444;
}
.status-dot.online {
background: #22c55e;
animation: blink 1s ease-in-out 3;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
/* Retry button */
.retry-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: #111;
color: #fff;
border: none;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: opacity 0.15s;
}
.retry-btn:active {
opacity: 0.85;
}
.retry-btn svg {
width: 16px;
height: 16px;
}
.retry-btn.spinning svg {
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Cached section */
.cached-section {
width: 100%;
text-align: left;
margin-top: 8px;
}
.cached-section h2 {
font-size: 13px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 10px;
}
.cached-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.cached-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #fff;
border-radius: 10px;
border: 1px solid #e5e7eb;
cursor: pointer;
transition: border-color 0.15s;
}
.cached-item:hover {
border-color: #6366f1;
}
.cached-item svg {
width: 18px;
height: 18px;
color: #9ca3af;
flex-shrink: 0;
}
.cached-item strong {
font-size: 14px;
color: #111;
display: block;
}
.cached-item span {
font-size: 12px;
color: #aaa;
}const retryBtn = document.getElementById("retryBtn");
const statusDot = document.getElementById("statusDot");
const statusText = document.getElementById("statusText");
let polling = null;
function setOnline() {
statusDot.className = "status-dot online";
statusText.textContent = "Back online! Redirecting…";
clearInterval(polling);
setTimeout(() => {
// In a real PWA: window.location.reload();
statusText.textContent = "Connected";
}, 1500);
}
function checkConnection() {
// Demo: simulate reconnect after a few seconds
fetch("/", { method: "HEAD", cache: "no-store" })
.then(() => setOnline())
.catch(() => {});
}
retryBtn.addEventListener("click", () => {
retryBtn.classList.add("spinning");
retryBtn.disabled = true;
setTimeout(() => {
retryBtn.classList.remove("spinning");
retryBtn.disabled = false;
// In a real app: window.location.reload();
}, 1500);
});
// Poll every 5 seconds
polling = setInterval(checkConnection, 5000);
window.addEventListener("online", setOnline);<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
<title>You're Offline</title>
</head>
<body>
<div class="page">
<div class="logo">Brand</div>
<main class="content">
<div class="wifi-icon" aria-hidden="true">
<svg viewBox="0 0 80 80" fill="none">
<circle cx="40" cy="40" r="36" fill="#f9fafb" stroke="#e5e7eb" stroke-width="2"/>
<!-- WiFi arcs — muted (offline state) -->
<path d="M22 38c4.8-4.8 11.4-7.8 18-7.8s13.2 3 18 7.8" stroke="#d1d5db" stroke-width="2.5" stroke-linecap="round"/>
<path d="M27 43c3.3-3.3 7.9-5.4 13-5.4s9.7 2.1 13 5.4" stroke="#d1d5db" stroke-width="2.5" stroke-linecap="round"/>
<path d="M32 48c2-2 4.8-3.2 8-3.2s6 1.2 8 3.2" stroke="#d1d5db" stroke-width="2.5" stroke-linecap="round"/>
<circle cx="40" cy="54" r="2.5" fill="#9ca3af"/>
<!-- X indicator -->
<path d="M28 28l8 8M36 28l-8 8" stroke="#ef4444" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</div>
<h1>You're offline</h1>
<p>Check your internet connection and try again. Some cached content is still available below.</p>
<div class="status-row">
<div class="status-dot offline" id="statusDot"></div>
<span id="statusText">No internet connection</span>
</div>
<button class="retry-btn" id="retryBtn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-4.95"/></svg>
Try Again
</button>
<div class="cached-section">
<h2>Available offline</h2>
<ul class="cached-list">
<li class="cached-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<div><strong>Home</strong><span>Cached 2 min ago</span></div>
</li>
<li class="cached-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<div><strong>Getting Started</strong><span>Cached 5 min ago</span></div>
</li>
<li class="cached-item">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<div><strong>API Reference</strong><span>Cached 1 hr ago</span></div>
</li>
</ul>
</div>
</main>
</div>
<script src="script.js"></script>
</body>
</html>Offline Page
A PWA offline fallback page served by the service worker when there is no network connection. Automatically polls for reconnection and shows recently cached pages.
Features
- Animated WiFi signal illustration
- Polls
navigator.onLineandfetchto detect reconnection - Shows a list of recently cached pages the user can still access
- Auto-redirects back to the original URL when reconnected
Service worker integration
Register this page as the offline fallback in your service worker:
// sw.js
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).catch(() => caches.match('/offline'))
);
});
When to use it
- Any PWA with a service worker
- Apps that need graceful offline degradation