|$ curl https://forge-ai.dev/api/markdown?path=docs/html/storage
$cat docs/web-storage-api.md
updated This week·40 min read·published

Web Storage API

HTMLStorageIntermediateIntermediate
Introduction

The Web Storage API provides a way for websites to store key-value pairs locally in a user's browser — persisting across page reloads and, in the case of localStorage, across browser sessions. Unlike cookies, data stored via the Web Storage API is never automatically sent to the server, making it a more efficient choice for client-side data that doesn't need server access.

The API exposes two storage mechanisms: localStorage (persistent, origin-scoped) and sessionStorage (tab-scoped, cleared when the tab closes). Both share the same interface — setItem(), getItem(), removeItem(), clear(), and key() — but differ in their lifetime and scope.

localStorage

localStorage stores data with no expiration time. Data persists even after the browser is closed and reopened, remaining available until explicitly deleted via JavaScript or the user clears their browser data. It is scoped to the origin (protocol + hostname + port), meaning https://example.com and https://app.example.com have separate storage.

localstorage.html
HTML
1<!-- localStorage persists across sessions -->
2<script>
3 // Store data
4 localStorage.setItem('theme', 'dark');
5 localStorage.setItem('fontSize', '16');
6
7 // Retrieve data
8 const theme = localStorage.getItem('theme');
9 console.log(theme); // "dark"
10
11 // Check if a key exists
12 if (localStorage.getItem('theme') !== null) {
13 console.log('Theme preference found');
14 }
15
16 // Get number of stored items
17 console.log(localStorage.length); // 2
18
19 // Remove a specific key
20 localStorage.removeItem('fontSize');
21
22 // Clear all data for this origin
23 localStorage.clear();
24</script>
25
26// Storage origin isolation examples:
27// https://example.com → separate from
28// https://app.example.com → separate from
29// https://example.com:8080 → separate from
30// http://example.com → separate (different protocol)
31
32// Data persists until explicitly removed:
33// - JavaScript: localStorage.removeItem() or .clear()
34// - User: "Clear browsing data" in browser settings
35// - User: "Clear site data" from site permissions

info

localStorage is synchronous and blocks the main thread. For large amounts of data or frequent read/write operations, consider wrapping access in a microtask (queueMicrotask or setTimeout) or using the IndexedDB API instead, which is asynchronous and non-blocking.
sessionStorage

sessionStorage is scoped to the current browser tab or window. Data survives page reloads and restores (e.g., after a browser crash with session restore) but is cleared when the tab is closed. Opening the same page in a new tab or window creates a new, empty sessionStorage. This makes it ideal for temporary, tab-specific state.

sessionstorage.html
HTML
1<!-- sessionStorage is tab-scoped -->
2<script>
3 // Tab A: set data
4 sessionStorage.setItem('formDraft', '{"step": 3, "name": "Alice"}');
5
6 // Tab B (same origin, different tab): null
7 console.log(sessionStorage.getItem('formDraft'));
8 // → null — each tab has its own sessionStorage
9
10 // Useful for multi-step form state
11 function saveDraft(step, data) {
12 sessionStorage.setItem('formStep', step);
13 sessionStorage.setItem('formData', JSON.stringify(data));
14 }
15
16 function restoreDraft() {
17 const step = sessionStorage.getItem('formStep');
18 const data = JSON.parse(sessionStorage.getItem('formData'));
19 if (step && data) {
20 return { step: parseInt(step), data };
21 }
22 return null;
23 }
24
25 // Wizard form example
26 // Step 1 → saveDraft(1, { name: 'Alice' })
27 // User navigates to Step 2 → saveDraft(2, { name: 'Alice', email: 'a@b.com' })
28 // User accidentally refreshes → restoreDraft() picks up at step 2
29
30 // Tab-scoped authentication token (short-lived)
31 sessionStorage.setItem('sessionToken', 'eyJhbGciOi...');
32</script>
33
34// sessionStorage lifecycle:
35// Tab opened → empty sessionStorage
36// Page reload → data preserved
37// Tab closed → data destroyed
38// New tab → new empty sessionStorage
🔥

pro tip

Use sessionStorage for multi-step form wizards, checkout flows, and any state that should not persist after the user closes the tab. Unlike localStorage, session-scoped data is automatically cleaned up, reducing the risk of stale data leaking across user sessions on shared devices.
Methods

Both localStorage and sessionStorage share the same five methods on the Storage interface. Values are always stored as strings — non-string values are automatically converted via toString(). Use JSON.stringify() and JSON.parse() for complex data like objects and arrays.

MethodSignatureDescription
setItemsetItem(key, value)Store a key-value pair (value is stringified)
getItemgetItem(key)Retrieve value by key (null if missing)
removeItemremoveItem(key)Delete a single key-value pair
clearclear()Remove all key-value pairs for the origin
keykey(index)Get the key at the given numeric index
lengthlength (property)Number of stored key-value pairs
storage-crud.html
HTML
1<!-- Complete CRUD operations for Web Storage -->
2<script>
3 // CREATE — store data
4 localStorage.setItem('username', 'alice');
5 localStorage.setItem('preferences', JSON.stringify({
6 theme: 'dark',
7 fontSize: 14,
8 notifications: true,
9 }));
10
11 // READ — retrieve data
12 const username = localStorage.getItem('username');
13 // "alice"
14
15 const prefs = JSON.parse(
16 localStorage.getItem('preferences')
17 );
18 // { theme: 'dark', fontSize: 14, notifications: true }
19
20 // READ — iterate over all keys
21 for (let i = 0; i < localStorage.length; i++) {
22 const key = localStorage.key(i);
23 const value = localStorage.getItem(key);
24 console.log(`${key}: ${value}`);
25 }
26
27 // READ — using Object.entries pattern
28 const allData = { ...localStorage };
29 console.log(allData);
30 // { username: 'alice', preferences: '{"theme":"dark",...}' }
31
32 // UPDATE — overwrite existing key
33 const updatedPrefs = {
34 ...JSON.parse(localStorage.getItem('preferences')),
35 fontSize: 16, // change font size
36 };
37 localStorage.setItem('preferences', JSON.stringify(updatedPrefs));
38
39 // DELETE — remove one item
40 localStorage.removeItem('username');
41
42 // DELETE — remove all items
43 // localStorage.clear();
44
45 // Check existence
46 if (localStorage.getItem('preferences') !== null) {
47 console.log('Preferences exist');
48 }
49
50 // Safe retrieval with fallback
51 function getStorageItem(key, fallback = null) {
52 try {
53 const item = localStorage.getItem(key);
54 return item !== null ? JSON.parse(item) : fallback;
55 } catch {
56 return fallback;
57 }
58 }
59
60 const config = getStorageItem('config', { theme: 'light' });
61</script>
Storage Events

The storage event fires on other window objects (tabs or iframes) of the same origin when storage changes. The event does not fire on the tab that made the change — it is specifically for cross-tab synchronization. This enables powerful patterns like sharing state between open tabs without a server.

storage-events.html
HTML
1<!-- Cross-tab synchronization via storage event -->
2<script>
3 // Listen for storage changes from other tabs
4 window.addEventListener('storage', (event) => {
5 console.log('Storage changed:', {
6 key: event.key, // The key that changed
7 oldValue: event.oldValue, // Previous value (null if new)
8 newValue: event.newValue, // New value (null if deleted)
9 url: event.url, // URL of the tab that made the change
10 storageArea: event.storageArea, // localStorage or sessionStorage
11 });
12
13 // React to specific changes
14 if (event.key === 'theme') {
15 applyTheme(event.newValue);
16 }
17 if (event.key === 'logout') {
18 window.location.reload();
19 }
20 });
21
22 function applyTheme(theme) {
23 document.documentElement.setAttribute('data-theme', theme);
24 }
25
26 // Tab A: changes theme
27 document.getElementById('themeToggle').addEventListener('click', () => {
28 const newTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
29 localStorage.setItem('theme', newTheme);
30 applyTheme(newTheme);
31 // storage event fires in Tab B, C, D... but NOT in Tab A
32 });
33
34 // Cross-tab logout
35 function logoutAllTabs() {
36 localStorage.setItem('logout', Date.now().toString());
37 // All other tabs will detect this and redirect
38 window.location.href = '/login';
39 }
40
41 // Broadcast channel alternative (modern browsers)
42 const channel = new BroadcastChannel('app-sync');
43 channel.postMessage({ type: 'THEME_CHANGE', theme: 'dark' });
44 channel.onmessage = (event) => {
45 if (event.data.type === 'THEME_CHANGE') {
46 applyTheme(event.data.theme);
47 }
48 };
49</script>

best practice

The storage event is the simplest way to synchronize state across tabs. However, BroadcastChannel API offers a more flexible alternative for real-time communication between browsing contexts. For complex cross-tab state management (shared shopping cart, real-time collaboration), consider using IndexedDB with BroadcastChannel for coordination.
Storage Limits

Web Storage is limited to approximately 5 MB per origin across most browsers. The limit applies to the total size of all key-value pairs (including both key and value strings in UTF-16 encoding). Attempting to exceed this limit throws a QuotaExceededError (DOMException 22). The actual limit varies slightly between browsers and can change based on available disk space.

BrowserStorage LimitScope
Chrome~5 MBPer origin (shared across all tabs)
Firefox~5 MBPer origin
Safari~5 MBPer origin (Desktop); ~2 MB (iOS)
Edge~5 MBPer origin
iOS Safari~2 MBPer origin (limited storage)
storage-limits.html
HTML
1<!-- Storage limit detection and error handling -->
2<script>
3 function isStorageAvailable(type = 'localStorage') {
4 try {
5 const storage = window[type];
6 const testKey = '__storage_test__';
7 storage.setItem(testKey, 'test');
8 storage.removeItem(testKey);
9 return true;
10 } catch (e) {
11 return false;
12 }
13 }
14
15 function setWithQuotaCheck(key, value) {
16 try {
17 localStorage.setItem(key, value);
18 return true;
19 } catch (e) {
20 if (e.name === 'QuotaExceededError' ||
21 e.code === 22) {
22 console.warn('Storage quota exceeded');
23 // Clean up old/unused data
24 cleanupOldStorage();
25 return false;
26 }
27 console.error('Storage error:', e);
28 return false;
29 }
30 }
31
32 function getStorageUsage() {
33 let total = 0;
34 for (let i = 0; i < localStorage.length; i++) {
35 const key = localStorage.key(i);
36 const value = localStorage.getItem(key);
37 // Estimate: 2 bytes per character (UTF-16)
38 total += (key.length + value.length) * 2;
39 }
40 return {
41 bytes: total,
42 kilobytes: (total / 1024).toFixed(1),
43 megabytes: (total / (1024 * 1024)).toFixed(3),
44 percent: ((total / (5 * 1024 * 1024)) * 100).toFixed(1),
45 };
46 }
47
48 // Monitor storage usage
49 console.log(getStorageUsage());
50 // { bytes: 1402, kilobytes: "1.4", megabytes: "0.001", percent: "0.0" }
51</script>

warning

Always wrap storage setItem() calls in a try-catch block. The QuotaExceededError can fire not only when storage is full, but also when private browsing modes in some browsers completely disable storage. Never assume storage is available — defensive coding prevents silent data loss.
JSON Serialization

Web Storage only supports string values. Storing objects, arrays, numbers, or booleans requires explicit serialization with JSON.stringify() and deserialization with JSON.parse(). Without serialization, objects are converted to the string "[object Object]", and arrays are joined with commas.

json-serialization.html
HTML
1<!-- JSON serialization for complex data -->
2<script>
3 // WRONG — silently corrupts data
4 localStorage.setItem('user', { name: 'Alice', age: 30 });
5 console.log(localStorage.getItem('user'));
6 // → "[object Object]" ✗
7
8 // WRONG — array becomes comma-separated string
9 localStorage.setItem('tags', ['html', 'css', 'js']);
10 console.log(localStorage.getItem('tags'));
11 // → "html,css,js" ✗ (cannot distinguish from actual string)
12
13 // CORRECT — serialize with JSON.stringify
14 const user = {
15 id: 42,
16 name: 'Alice',
17 age: 30,
18 roles: ['admin', 'editor'],
19 settings: {
20 theme: 'dark',
21 notifications: true,
22 },
23 };
24 localStorage.setItem('user', JSON.stringify(user));
25
26 // CORRECT — deserialize with JSON.parse
27 const stored = JSON.parse(localStorage.getItem('user'));
28 console.log(stored.name); // "Alice"
29 console.log(stored.roles); // ["admin", "editor"]
30
31 // Safe deserialization with fallback
32 function safeParse(value, fallback = null) {
33 try {
34 return JSON.parse(value);
35 } catch {
36 return fallback;
37 }
38 }
39
40 // Storing primitive values (they stringify correctly)
41 localStorage.setItem('count', 42);
42 // Stored as "42" (string)
43 const count = parseInt(localStorage.getItem('count'), 10);
44 // 42 (number)
45
46 // Boolean handling
47 localStorage.setItem('isActive', true);
48 // Stored as "true" (string)
49 const isActive = localStorage.getItem('isActive') === 'true';
50 // true (boolean)
51</script>
Security

Web Storage is not encrypted — data is stored in plain text within the browser's profile directory on disk. Any JavaScript running on the same origin can read, modify, or delete all storage data. This makes storage a prime target for XSS (Cross-Site Scripting) attacks. Never store sensitive data like authentication tokens, API keys, personal information, or credit card numbers in Web Storage.

ThreatRiskMitigation
XSS attacksAttacker reads all storageSanitize all inputs; use CSP headers
Shared devicesNext user can read storageClear storage on logout; use sessionStorage
Malicious extensionsBrowser extensions read storageNever store secrets; use httpOnly cookies for auth
Same-origin iframesEmbedded content can access storageVerify iframe origin; use CSP frame-ancestors
Physical accessStorage read from diskNever store sensitive data

warning

Web Storage is NOT a secure storage mechanism. An XSS vulnerability gives attackers full read/write access to all storage data. For authentication, use httpOnly cookies (which cannot be read by JavaScript) instead of storing tokens in localStorage. If you must store sensitive data, consider using the Web Crypto API or the Credential Management API.
Use Cases

Web Storage is best suited for non-sensitive, client-only data that improves user experience. Common use cases include theme preferences, form draft recovery, UI state persistence, feature flags for A/B testing, and caching computed data.

storage-use-cases.html
HTML
1<!-- Theme persistence with localStorage -->
2<script>
3 // Apply saved theme on page load
4 (function initTheme() {
5 const savedTheme = localStorage.getItem('theme') || 'dark';
6 document.documentElement.setAttribute('data-theme', savedTheme);
7 })();
8
9 // Toggle theme and persist
10 document.getElementById('themeToggle').addEventListener('click', () => {
11 const current = document.documentElement.getAttribute('data-theme');
12 const next = current === 'dark' ? 'light' : 'dark';
13 document.documentElement.setAttribute('data-theme', next);
14 localStorage.setItem('theme', next);
15 });
16
17 // Form draft auto-save
18 const form = document.getElementById('signupForm');
19 const DRAFT_KEY = 'signupDraft';
20
21 // Auto-restore draft on page load
22 function restoreDraft() {
23 const draft = localStorage.getItem(DRAFT_KEY);
24 if (draft) {
25 const data = JSON.parse(draft);
26 Object.entries(data).forEach(([name, value]) => {
27 const input = form.elements[name];
28 if (input) input.value = value;
29 });
30 }
31 }
32 restoreDraft();
33
34 // Auto-save on input change (debounced)
35 let saveTimeout;
36 form.addEventListener('input', () => {
37 clearTimeout(saveTimeout);
38 saveTimeout = setTimeout(() => {
39 const data = new FormData(form);
40 const obj = Object.fromEntries(data.entries());
41 localStorage.setItem(DRAFT_KEY, JSON.stringify(obj));
42 }, 500);
43 });
44
45 // Clear draft on successful submit
46 form.addEventListener('submit', () => {
47 localStorage.removeItem(DRAFT_KEY);
48 });
49
50 // Feature flags for gradual rollout
51 const flags = {
52 newCheckout: true,
53 redesignedDashboard: false,
54 betaSearch: true,
55 };
56 localStorage.setItem('featureFlags', JSON.stringify(flags));
57
58 function isFeatureEnabled(flag) {
59 const stored = localStorage.getItem('featureFlags');
60 if (!stored) return false;
61 return JSON.parse(stored)[flag] === true;
62 }
63
64 // UI state persistence (sidebar collapsed, panel order)
65 function saveSidebarState(collapsed) {
66 localStorage.setItem('sidebarCollapsed', collapsed);
67 }
68 function loadSidebarState() {
69 return localStorage.getItem('sidebarCollapsed') === 'true';
70 }
71</script>

Live preview showing a persistent theme toggle using localStorage:

preview

info

Always debounce frequent writes like form auto-saves or scroll positions — writing to storage on every keystroke or scroll event can degrade performance. A 300-500 ms debounce threshold balances responsiveness with write frequency. For very frequent state updates (animation, real-time typing), consider in-memory state with periodic storage snapshots instead.
IndexedDB vs Storage vs Cookies

Choosing the right client-side storage mechanism depends on data size, lifetime, accessibility requirements, and performance needs. Each option has distinct trade-offs — Web Storage is simple and synchronous but limited to 5 MB of string data; IndexedDB is asynchronous and handles large structured data but has a more complex API; Cookies are automatically sent with requests but are limited to 4 KB.

FeaturelocalStoragesessionStorageIndexedDBCookies
Storage Limit~5 MB~5 MBUnlimited (usually ~50% of disk)~4 KB
Data TypeStrings onlyStrings onlyAny structured data (objects, blobs)Strings only
AsyncSynchronous (blocking)Synchronous (blocking)Asynchronous (non-blocking)Synchronous
Auto-sent to serverNoNoNoYes (with every request)
ScopeOriginTab / windowOriginOrigin + path (configurable)
ExpirationManual clear onlyOn tab closeManual clear onlyConfigurable (expires/max-age)
Accessible fromJavaScript (same-origin)JavaScript (same-tab)JavaScript (same-origin)JavaScript + Server (httpOnly option)
API ComplexitySimple (synchronous)Simple (synchronous)Complex (event-based/promises)Simple (document.cookie)
Indexes / QueriesNoNoYes (indexes, range queries, cursors)No
Use CasePreferences, UI stateForm drafts, tab stateOffline data, large datasetsAuth tokens, tracking
🔥

pro tip

For most web applications, use a layered approach: sessionStorage for ephemeral tab state, localStorage for user preferences and UI state, IndexedDB for offline-first data and large datasets, and httpOnly cookies for authentication tokens. Never store secrets in localStorage or sessionStorage.
Best Practices

Storage Checklist

Always use try-catch around setItem — storage can throw (quota exceeded, disabled, private mode)
Never store sensitive data (auth tokens, passwords, PII) in localStorage or sessionStorage
Use httpOnly cookies for authentication tokens — they cannot be read by JavaScript
Always serialize objects with JSON.stringify and deserialize with JSON.parse
Use sessionStorage for tab-scoped ephemeral state (form wizards, checkout flow)
Use localStorage for persistent, non-sensitive data (theme, UI prefs, feature flags)
Use IndexedDB for large datasets (>100 KB), offline storage, or structured data with indexes
Implement storage usage monitoring to detect approaching quota limits
Debounce frequent writes (form auto-save, scroll position) to avoid performance degradation
Clear storage on user logout to prevent data leakage on shared devices
Check storage availability on page load — private browsing may disable storage
Use semantic key naming conventions (e.g., appName:feature:key) to avoid collisions
Version your stored data schema to handle migration when the app evolves

Related Topics

$Blueprint — Engineering Documentation·Section ID: HTML-32·Revision: 1.0