An HTML app works offline when every file it loads is inside the APK and its data lives on the device. Bundle all scripts, styles, fonts and images in the ZIP instead of loading them from CDNs, store user data in localStorage or IndexedDB, cache the last API response, and show a clear message for features that genuinely need the network.
Offline has three layers
- The shell — HTML, CSS, JS, fonts, icons. Bundled in the APK, always available. This is free with any ZIP or HTML build.
- Your data — what the user creates: notes, scores, settings. Stored on the device.
- Remote data — what comes from your server. Cached, so the app shows the last copy.
Most "offline" bugs are layer 1 failures nobody noticed because the developer's phone was always online.
Layer 1: find and bundle every external file
Search your HTML and CSS for https://. Typical offenders: jQuery or Bootstrap from a CDN, Google Fonts, Font Awesome, analytics, a chart library, an icon set loaded with @import. The CDN dependency scanner lists them all with the local path to use. Download each file into your project, point the tag at it, and rebuild.
<!-- before: breaks offline -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet">
<!-- after: bundled -->
<script src="vendor/chart.umd.min.js"></script>
<link href="fonts/inter.css" rel="stylesheet">For Google Fonts, download the WOFF2 files (google-webfonts-helper or the font's GitHub release) and write a small @font-face rule.
Layer 2: storage on the device
| API | Capacity | Use for |
|---|---|---|
localStorage | ~5 MB of strings | Settings, small lists, game saves |
| IndexedDB | Hundreds of MB | Records, images, cached API data |
| Cache API | Shared with IndexedDB | Cached responses (rarely needed in a bundled app) |
All three are private to the app, persist across launches and survive app updates. They are cleared when the user uninstalls or taps Clear storage. The app's storage is separate from Chrome's, so a user starts empty even if they used your website before.
Layer 3: remote data that degrades gracefully
async function getMenu() {
try {
const res = await fetch('https://api.example.com/menu', { cache: 'no-store' });
if (!res.ok) throw new Error(res.status);
const data = await res.json();
localStorage.setItem('menu', JSON.stringify(data));
return { data, fresh: true };
} catch (e) {
const cached = localStorage.getItem('menu');
if (cached) return { data: JSON.parse(cached), fresh: false };
throw e; // nothing cached yet: show an offline state
}
}Show a small "showing saved data" banner when fresh is false, and listen to the online event to refresh. Never block the whole app on a request that may never complete.
Testing offline properly
- Install the APK, switch on airplane mode, then launch it for the first time.
- Visit every screen. Anything blank or unstyled is an external file you missed.
- Create data, force-stop the app, relaunch — the data must still be there.
- Install the next build over the top and check the data again.
Questions people ask
Does an APK made from HTML work without internet?
Yes, if the HTML and every file it uses are bundled inside it. Files loaded from other servers — CDN libraries, web fonts, APIs — need a connection unless you bundle or cache them.
Where should an offline HTML app store data?
localStorage for small amounts of text such as settings and saves; IndexedDB for anything larger. Both persist across app launches and updates.
Do I need a service worker for an offline APK?
No. A service worker caches files that would otherwise come from the network; in a bundled app they are already on the device.