localStorage β Persistent Storage
localStorage stores data with no expiration time. Data persists across browser sessions, tabs (same origin), and restarts. The limit is approximately 5 MB per origin (varies by browser).
// setItem β store a value (strings only!)
localStorage.setItem('username', 'alice');
localStorage.setItem('theme', 'dark');
// getItem β retrieve (returns null if key doesn't exist)
const username = localStorage.getItem('username');
console.log(username); // "alice"
const missing = localStorage.getItem('nonexistent');
console.log(missing); // null
// removeItem β delete a single key
localStorage.removeItem('theme');
// clear β delete ALL keys in this origin (use with caution!)
// localStorage.clear();
// length β number of stored items
console.log(localStorage.length); // 1
// key(index) β get key name by index (order not guaranteed)
console.log(localStorage.key(0)); // "username"
// Direct property access (works but not recommended β conflicts with built-in props)
localStorage.color = 'blue'; // β avoid
localStorage.setItem('color', 'blue'); // β
preferred
username β "alice" missing β null length β 1 key(0) β "username"
sessionStorage β Tab-Scoped Storage
sessionStorage has the same API as localStorage but data is cleared when the tab (or window) is closed. Opening the same URL in a new tab creates a completely separate storage scope.
// Identical API to localStorage
sessionStorage.setItem('currentStep', '2');
sessionStorage.setItem('formDraft', JSON.stringify({ name: 'Bob', email: 'bob@mail.com' }));
const step = sessionStorage.getItem('currentStep');
console.log(step); // "2"
// Use cases:
// - Wizard/multi-step form progress (lose if user closes tab β by design)
// - Temporary shopping cart (before checkout)
// - One-time messages / alerts per session
// - Protecting against CSRF tokens (very short-lived)
sessionStorage.removeItem('currentStep');
// Cleared automatically when tab closes β no manual cleanup needed
Storing Objects with JSON
Web Storage only stores strings. Use JSON.stringify() to store objects and JSON.parse() to restore them:
// Store an object
const user = { name: 'Alice', role: 'admin', preferences: { theme: 'dark', lang: 'en' } };
localStorage.setItem('user', JSON.stringify(user));
// Retrieve and parse
const stored = localStorage.getItem('user');
const parsedUser = stored ? JSON.parse(stored) : null;
console.log(parsedUser.preferences.theme); // "dark"
// Store an array
const history = ['home', 'about', 'products'];
localStorage.setItem('navHistory', JSON.stringify(history));
const navHistory = JSON.parse(localStorage.getItem('navHistory') || '[]');
navHistory.push('contact');
localStorage.setItem('navHistory', JSON.stringify(navHistory));
// Helper wrapper to avoid repetition
const store = {
set(key, val) { localStorage.setItem(key, JSON.stringify(val)); },
get(key, fallback = null) {
const item = localStorage.getItem(key);
return item !== null ? JSON.parse(item) : fallback;
},
remove(key) { localStorage.removeItem(key); }
};
store.set('settings', { fontSize: 16, color: '#333' });
const settings = store.get('settings', { fontSize: 14, color: '#000' });
The storage Event β Cross-Tab Sync
When localStorage is changed in one tab, a storage event fires in all other tabs of the same origin β enabling real-time cross-tab communication:
// Listen for changes made in OTHER tabs
window.addEventListener('storage', (event) => {
console.log('Key changed:', event.key);
console.log('Old value:', event.oldValue);
console.log('New value:', event.newValue);
console.log('Storage area:', event.storageArea); // localStorage or sessionStorage
console.log('Origin URL:', event.url);
// Example: sync theme across tabs
if (event.key === 'theme') {
document.body.dataset.theme = event.newValue;
}
// Cross-tab logout
if (event.key === 'auth_token' && event.newValue === null) {
redirectToLogin();
}
});
// Note: the storage event does NOT fire in the tab that made the change
// It ONLY fires in other tabs/windows
Store the theme choice in localStorage and read it on page load before rendering to avoid flash of wrong theme (FOUC). Apply the theme class to <html> or <body> at the very top of <head> β before any CSS is applied.
IndexedDB β Large Structured Data
IndexedDB is a browser-native NoSQL database for large volumes of structured data. It supports transactions, indexes, cursors, and blob storage β but has a complex API. Libraries like Dexie.js make it practical:
// Raw IndexedDB API (simplified example)
const request = indexedDB.open('myDatabase', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create an object store (like a table)
const store = db.createObjectStore('users', { keyPath: 'id' });
store.createIndex('email', 'email', { unique: true });
};
request.onsuccess = (event) => {
const db = event.target.result;
// Write a record
const tx = db.transaction('users', 'readwrite');
tx.objectStore('users').put({ id: 1, name: 'Alice', email: 'alice@example.com' });
tx.oncomplete = () => console.log('User saved');
};
// Using Dexie.js (much cleaner):
// const db = new Dexie('myDatabase');
// db.version(1).stores({ users: '++id, name, email' });
// await db.users.add({ name: 'Alice', email: 'alice@example.com' });
// const alice = await db.users.where('email').equals('alice@example.com').first();
Storage Options Comparison
| Feature | Cookies | localStorage | sessionStorage | IndexedDB |
|---|---|---|---|---|
| Capacity | ~4 KB | ~5 MB | ~5 MB | Hundreds of MB+ |
| Lifetime | Set by expiry / session | Permanent | Until tab closes | Permanent |
| Sent with requests | Yes (automatic) | No | No | No |
| Accessible from JS | Yes (unless HttpOnly) | Yes | Yes | Yes (async) |
| Cross-tab sharing | Yes | Yes (same origin) | No | Yes (same origin) |
| Data format | String | String | String | Any structured data |
| Use case | Auth, tracking | Preferences, cache | Form draft, wizard | Offline apps, large data |
localStorage and sessionStorage are accessible by any JavaScript on the page, including injected third-party scripts. Never store passwords, authentication tokens (JWTs), credit card numbers, or any personally identifiable information. For auth tokens, prefer HttpOnly cookies which are invisible to JavaScript.
Practical: Persisting Theme and Preferences
// Theme persistence
const themeBtn = document.getElementById('themeToggle');
// Apply saved theme on load
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
themeBtn.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
// Persist font size preference
const fontSizeControl = document.getElementById('fontSize');
const savedSize = localStorage.getItem('fontSize') || '16';
document.body.style.fontSize = savedSize + 'px';
fontSizeControl.value = savedSize;
fontSizeControl.addEventListener('input', () => {
const size = fontSizeControl.value;
document.body.style.fontSize = size + 'px';
localStorage.setItem('fontSize', size);
});
localStorage vs sessionStorage: Persisting Data in the Browser
Web Storage saves key/value data in the browser. The two flavors differ only in lifetime: localStorage persists until explicitly cleared (survives restarts); sessionStorage lasts only for that tab's session.
localStorage.setItem("theme", "dark");
localStorage.getItem("theme"); // "dark"
localStorage.removeItem("theme");
// Values are STRINGS ONLY β objects must be serialized
localStorage.setItem("user", JSON.stringify({ name: "Ann" }));
const user = JSON.parse(localStorage.getItem("user"));
| localStorage | sessionStorage | |
|---|---|---|
| Lifetime | until cleared | until tab closes |
| Shared across tabs | yes (same origin) | no |
| Size | ~5-10 MB per origin | |
Three things to know: everything is stored as a string, so wrap objects in JSON.stringify/parse (forgetting this stores "[object Object]"). The API is synchronous β writing megabytes blocks the main thread, so keep it small. And it's not secure: any JavaScript on the page (including XSS) can read it, so never store tokens, passwords, or sensitive data there. Listen for the storage event to sync state across tabs.
ποΈ Practical Exercise
Build a persistent notes app:
- A
<textarea>for note input and a "Save" button. - On save, add the note to an array stored in localStorage under the key
'notes'. - On page load, read the array from localStorage and render all notes as cards.
- Each card has a "Delete" button that removes the note from the array and updates localStorage.
- Show the count of saved notes.
π₯ Challenge Exercise
Build a multi-tab counter using the storage event. Page A has increment/decrement buttons. Page B displays the current count. Any change on page A should instantly update page B via the storage event (without polling). Both pages should also persist the count across page refreshes. Test by opening both pages in separate tabs.
Summary
π Summary
localStorage: persistent, ~5 MB, same origin, string-only values.sessionStorage: same API, cleared when tab closes, not shared between tabs.- Use
JSON.stringify/parseto store and retrieve objects and arrays. - The
storageevent fires in other tabs when localStorage changes β useful for cross-tab sync. - IndexedDB handles large structured data; use Dexie.js to simplify its API.
- Never store passwords or auth tokens in Web Storage β use
HttpOnlycookies instead. - Always provide a fallback value when reading from storage in case the key doesn't exist.
Interview Questions
- What is the difference between localStorage and sessionStorage?
- Why can't you store objects directly in localStorage?
- How would you implement cross-tab communication using Web Storage?
- Why should you avoid storing JWT tokens in localStorage?
- When would you choose IndexedDB over localStorage?
Related Topics
FAQ
The browser throws a DOMException with the name QuotaExceededError. Wrap write operations in a try/catch to handle this gracefully: catch the error, inform the user, or remove old data to make room with localStorage.removeItem() before retrying.
No. Web Storage is scoped to the origin (scheme + domain + port). https://example.com cannot read https://other.com's storage, and http://example.com cannot read https://example.com's storage. However, all scripts running on the same origin can access the same storage β including ads and analytics scripts loaded on your page.
Yes. In private/incognito mode, localStorage is isolated to the private session and is cleared when the window closes β behaving like sessionStorage. On some browsers (older Safari), localStorage.setItem() may throw in private mode due to quota being set to zero. Always wrap writes in try/catch.

