Building Native-Like Offline Experience in Next.js PWAs
Transform your PWA's offline experience from frustrating error pages to helpful, branded interactions that keep users engaged even without connectivity.
Start typing to search...
Progressive Web Apps (PWAs) are designed to deliver fast, reliable, and native-like experiences—even when users go offline. However, most implementations, including Sitecore-based PWAs, still fall short during network disruptions. Instead of helpful fallbacks, users often encounter generic browser error messages and broken interfaces.
In this guide, we’ll show you how to build a robust, native-like offline experience for Next.js PWAs powered by Sitecore. Using a multi-layered architecture—combining network detection hooks, intelligent service worker caching, branded offline modals, and static fallback pages—you’ll create an experience that keeps users informed, supported, and engaged, even when connectivity fails.
This approach is designed to integrate seamlessly into Sitecore XM Cloud or any Sitecore JSS-based PWA using Next.js.
Most web applications handle offline scenarios poorly, creating frustrating user experiences:
We'll build a comprehensive offline system that provides:
Here's how our multi-layered offline system works:

Image 1: Offline handling flow for a Sitecore-powered Next.js PWA using service worker fallback and in-app recovery.
When the PWA opens, it first checks for network availability. If online, the app runs normally with active monitoring for connection drops. If offline, the service worker checks whether resources are cached. Cached resources allow the app to load with an offline modal, while no cache triggers a static offline page. In both cases, users see a helpful offline UI with the option to “try again.” When retry is clicked, the app tests connectivity—if restored, the modal closes and the app resumes; if not, the app remains in offline mode until the connection is available again.
The network status hook is the app’s first line of defense against connectivity issues. It continuously monitors network status and tests real endpoints, allowing the app to quickly detect when a user goes offline and trigger fallback behavior instead of leaving them with generic errors.
export interface NetworkStatus {
isOnline: boolean;
connectionType: string;
effectiveType: string;
downlink: number;
rtt: number;
}
export const useNetworkStatus = () => {
const [networkStatus, setNetworkStatus] = useState<NetworkStatus>(() => ({
isOnline: typeof window !== "undefined" ? navigator.onLine : false,
connectionType: "unknown",
effectiveType: "unknown",
downlink: 0,
rtt: 0,
}));
const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false);
// Multi-endpoint connectivity testing
const testNetworkConnectivity = useCallback(async (): Promise<boolean> => {
if (!navigator.onLine) return false;
const testEndpoints = [
{ url: "/api/health", timeout: 3000 },
{ url: "https://www.google.com/favicon.ico", timeout: 5000 },
{ url: "https://httpbin.org/status/200", timeout: 5000 },
];
for (const { url, timeout } of testEndpoints) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
await fetch(url, {
method: "HEAD",
mode: url.startsWith("http") ? "no-cors" : "same-origin",
cache: "no-cache",
signal: controller.signal,
});
clearTimeout(timeoutId);
return true;
} catch (error) {
continue;
}
}
return false;
}, []);
return {
networkStatus,
isOnline: networkStatus.isOnline,
isOffline: !networkStatus.isOnline,
isOfflineModalVisible,
setIsOfflineModalVisible,
refreshNetworkStatus,
testNetworkConnectivity,
};
};
The service worker provides the backbone of offline support. By precaching critical resources and applying smart caching strategies, it ensures users can still load essential content and navigate the app—even when their connection drops completely.
Our service worker provides comprehensive caching and offline fallback:
// Import Workbox
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/6.5.4/workbox-sw.js"
);
// Precache essential resources
workbox.precaching.precacheAndRoute([
{ url: "/", revision: null },
{ url: "/offline.html", revision: null },
{ url: "/api/health", revision: null },
{ url: "/login", revision: null },
]);
// Cache strategies for different resource types
workbox.routing.registerRoute(
/\\.(?:js|css|worker\\.js)$/,
new workbox.strategies.StaleWhileRevalidate({
cacheName: "static-resources",
})
);
workbox.routing.registerRoute(
/\\.(?:png|gif|jpg|jpeg|svg|webp)$/,
new workbox.strategies.CacheFirst({
cacheName: "images",
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
// API caching with short expiration
workbox.routing.registerRoute(
/^\\/api\\//,
new workbox.strategies.NetworkFirst({
cacheName: "api-cache",
plugins: [
new workbox.expiration.ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
}),
],
})
);
// Offline fallback for navigation requests
workbox.routing.setCatchHandler(({ event }) => {
if (event.request.destination === "document") {
return caches.match("/offline.html");
}
return Response.error();
});
The offline modal is the user-facing layer of the offline experience. Instead of showing a broken interface, it presents a branded, informative UI with retry options and support contacts, giving users clear guidance when connectivity is lost.
const OfflineModal: React.FC<OfflineModalProps> = ({ isVisible, onRetry }) => {
const [isRetrying, setIsRetrying] = useState(false);
const handleRetry = async () => {
setIsRetrying(true);
try {
await onRetry();
} finally {
setIsRetrying(false);
}
};
if (!isVisible) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-80 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md mx-4">
<h2 className="text-lg font-bold mb-4">No Internet Connection</h2>
<p className="text-gray-600 mb-6">
You’re offline. Some content may be unavailable.
</p>
<div className="grid grid-cols-2 gap-4 mb-6">
<div>
<h3 className="font-semibold mb-2">Technical Support</h3>
<a href="tel:1-800-XXX-XXX" className="text-blue-600 underline">
1-800-XXX-XXXX
</a>
<p className="text-xs text-gray-500 mt-1">24/7 Support</p>
</div>
<div>
<h3 className="font-semibold mb-2">Customer Service</h3>
<a href="tel:1-800-XXX-XXXX" className="text-blue-600 underline">
1-800-XXX-XXXX
</a>
<p className="text-xs text-gray-500 mt-1">Mon-Fri 8AM-6PM</p>
</div>
</div>
<button
onClick={handleRetry}
disabled={isRetrying}
className="w-full bg-orange-500 text-white py-2 px-4 rounded hover:bg-orange-600 disabled:opacity-50"
>
{isRetrying ? "Testing..." : "Try Again"}
</button>
</div>
</div>
);
};
The static offline page acts as the final safety net when no cached resources are available. It delivers a lightweight, branded fallback with clear messaging, ensuring users always see something helpful instead of a generic browser error page.
When the service worker can't load the main application, it serves a static HTML page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Offline - Please Check Your Connection</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.modal {
background: white;
border-radius: 8px;
padding: 24px;
max-width: 400px;
width: 100%;
}
.contact-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin: 16px 0;
}
.phone-link {
color: #2563eb;
text-decoration: none;
font-weight: bold;
}
.retry-btn {
width: 100%;
background: #f97316;
color: white;
border: none;
padding: 12px;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="overlay">
<div class="modal">
<h1>No Internet Connection</h1>
<p>
We can't load content without internet. Please check your connection.
</p>
<div class="contact-grid">
<div>
<h3>Technical Support</h3>
<a href="tel:1-800-XXX-XXXX" class="phone-link">1-800-555-0123</a>
<p><small>24/7 Support</small></p>
</div>
<div>
<h3>Customer Service</h3>
<a href="tel:1-800-XXX-XXXX" class="phone-link">1-800-555-0456</a>
<p><small>Mon-Fri 8AM-6PM</small></p>
</div>
</div>
<button class="retry-btn" onclick="checkConnection()">Try Again</button>
</div>
</div>
<script>
async function checkConnection() {
const btn = document.querySelector(".retry-btn");
btn.textContent = "Testing...";
btn.disabled = true;
try {
await fetch("/api/health", { method: "HEAD" });
window.location.href = "/";
} catch (error) {
btn.textContent = "Try Again";
btn.disabled = false;
}
}
// Auto-recovery check every 30 seconds
setInterval(async () => {
if (navigator.onLine) {
try {
await fetch("/api/health", { method: "HEAD" });
window.location.href = "/";
} catch (error) {
// Still offline
}
}
}, 30000);
</script>
</body>
</html>
The auto-recovery system runs in the background to detect when connectivity is restored. It seamlessly closes the offline UI and returns the user to the full app without requiring a manual refresh, creating a native-like recovery flow.
Here's a simplified diagram showing the offline fallback flow:

/offline.html1. Launch Testing
# Test offline app launch
1. Disconnect internet completely
2. Open PWA in new browser tab
3. ✅ Should show offline modal within 1 second
4. ✅ Contact information should be clearly displayed
5. Reconnect internet and test retry functionality
2. Service Worker Testing
# Test service worker offline fallback
1. Disconnect internet
2. Refresh page or navigate to new URL
3. ✅ Should show /offline.html with matching design
4. ✅ Try Again button should work correctly
5. ✅ Should redirect to main app when online
3. Runtime Testing
# Test during normal app usage
1. Use app normally while online
2. Disconnect internet during active usage
3. ✅ Should show offline modal instead of error messages
4. ✅ Contact information should be easily accessible
5. Reconnect and verify seamless auto-recovery
// Test network status manually
window.navigator.onLine;
// Check service worker cache contents
caches.keys().then(console.log);
// Test connectivity endpoints directly
fetch("/api/health", { method: "HEAD" });
// Simulate offline/online events for testing
window.dispatchEvent(new Event("offline"));
window.dispatchEvent(new Event("online"));
This architecture helps you score 100 in the Lighthouse PWA category by covering offline support, service worker configuration, and fallback UI—key requirements for a reliable, installable experience.
// Typical service worker cache sizes
{
"static-resources": "2.3 MB", // CSS, JS, and worker files
"images": "5.1 MB", // Application images
"api-cache": "450 KB", // Cached API responses
"precache": "1.2 MB", // Essential pages
"google-fonts": "180 KB" // Font files
}
// Performance improvements
{
"offline_page_load": "< 100ms", // Instant loading from cache
"connectivity_detection": "< 1s", // Immediate offline detection
"auto_recovery": "< 30s", // Automatic online transition
"cache_hit_rate": "95%" // Successful offline operations
}
Start with the network detection hook and gradually add:
A reliable offline experience is no longer a nice-to-have—it's a user expectation. By implementing this multi-layered offline architecture in your Next.js Sitecore PWA, you're not just avoiding error pages—you're delivering a branded, seamless experience that keeps users informed and engaged, even without connectivity.
This approach turns frustrating dead ends into meaningful interactions, with auto-recovery, clear messaging, and native app-like behavior—all while improving your Lighthouse PWA score and overall performance.
Ready to go offline-first?
This guide gives you the foundation to build resilient, user-focused PWAs that work beautifully—online or off.
This implementation transforms frustrating offline experiences into helpful, branded interactions that guide users toward support and ensure a seamless return to full functionality when connectivity is restored.