Offline Status Indicator
Machine-coding brief for online/offline UI — navigator.onLine, events, banners, queue hooks, and a11y live regions.
- machine-coding
- interview
- react
- offline
Problem statement
Build an offline status indicator: detect browser online/offline, show a banner or badge, and optionally gate network actions. Interviewers score event subscription cleanup, honest limitations of navigator.onLine, and accessible announcements — not a full offline sync engine.
Requirements
Must have
- Reflect online vs offline status in the UI
- Subscribe to
windowonline/offlineevents - Initialize from
navigator.onLine - Accessible announcement when status changes
- Cleanup listeners on unmount
Should have
- Dismissible “Back online” toast that auto-hides
useOnlineStatushook reusable across app- Optional heartbeat fetch to detect “captive portal / lie-fi”
Nice to have
- Queue failed mutations while offline (interface only)
- Service worker integration note
- Last changed timestamp
Planning (5 minutes out loud)
navigator.onLineis imperfect — true doesn’t mean API works- Hook first — UI is a thin consumer
- Live region for status changes
- MVP — badge + banner on offline; then toast on reconnection
- Don’t block entire app unless product requires
Architecture
useOnlineStatus()
OnlineBanner / OfflineBadge
optional: createHeartbeatMonitor()
API
type OnlineStatus = {
online: boolean;
since: number; // timestamp of last change
};
function useOnlineStatus(): OnlineStatus;
Implementation sketch
Hook
function useOnlineStatus(): OnlineStatus {
const [online, setOnline] = useState(
typeof navigator !== "undefined" ? navigator.onLine : true
);
const [since, setSince] = useState(() => Date.now());
useEffect(() => {
function goOnline() {
setOnline(true);
setSince(Date.now());
}
function goOffline() {
setOnline(false);
setSince(Date.now());
}
window.addEventListener("online", goOnline);
window.addEventListener("offline", goOffline);
return () => {
window.removeEventListener("online", goOnline);
window.removeEventListener("offline", goOffline);
};
}, []);
return { online, since };
}
Banner UI
function ConnectivityBanner() {
const { online } = useOnlineStatus();
const [showBack, setShowBack] = useState(false);
const wasOffline = useRef(false);
useEffect(() => {
if (!online) {
wasOffline.current = true;
setShowBack(false);
return;
}
if (wasOffline.current) {
setShowBack(true);
const t = window.setTimeout(() => setShowBack(false), 3000);
return () => window.clearTimeout(t);
}
}, [online]);
if (!online) {
return (
<div className="banner offline" role="status" aria-live="assertive">
You are offline. Changes may not be saved.
</div>
);
}
if (showBack) {
return (
<div className="banner online" role="status" aria-live="polite">
Back online.
</div>
);
}
return null;
}
Optional heartbeat (lie-fi)
function useReachability(url = "/api/health", intervalMs = 15000) {
const [reachable, setReachable] = useState(true);
useEffect(() => {
let cancelled = false;
async function tick() {
try {
const res = await fetch(url, {
method: "HEAD",
cache: "no-store",
});
if (!cancelled) setReachable(res.ok);
} catch {
if (!cancelled) setReachable(false);
}
}
tick();
const id = window.setInterval(tick, intervalMs);
return () => {
cancelled = true;
window.clearInterval(id);
};
}, [url, intervalMs]);
return reachable;
}
Combine: show offline if !navigatorOnline || !reachable with careful UX to avoid flapping (require 2 failed heartbeats).
Gating actions
function SaveButton({ onSave }: { onSave: () => void }) {
const { online } = useOnlineStatus();
return (
<button type="button" disabled={!online} onClick={onSave}>
{online ? "Save" : "Save (unavailable offline)"}
</button>
);
}
Accessibility essentials
- Status changes go through
role="status"/aria-live - Offline:
assertiveis reasonable; back-online can bepolite - Don’t rely on color alone (icon + text)
- Disabled controls explain why
Performance notes
- Event listeners are cheap
- Heartbeat interval should be long; use exponential backoff on failure
- Avoid re-rendering whole app: put banner high in tree with narrow state subscription
Footguns
- Trusting
navigator.onLinealone in production sync-critical apps - Listener leaks
- SSR mismatch — default online, sync after mount
- Toast spam on flaky connections — debounce transitions
- Assuming fetch failures mean offline (could be 500)
Interview out-loud answer
I’d wrap
navigator.onLineplusonline/offlineevents in a hook, render a banner with a live region, and optionally confirm reachability with a cheap health check because lie-fi exists. MVP is indicator + disable destructive network actions. Full offline-first sync is a system-design follow-up with queues and conflict rules.
Related on this site
- Design Offline-first Notes App
- PWA Install and Offline Shell
- Toast Notification System
- Machine Coding Interview Framework