How to Use localStorage and sessionStorage in JavaScript
Cookies were the web's first client-side storage mechanism, but their limitations (4 KB size, sent with every HTTP request, awkward API) made them a poor fit for storing larger amounts of data that only the client needs. The Web Storage API was introduced to solve this: localStorage and sessionStorage provide a simple, synchronous key-value store with significantly more capacity and a cleaner interface.
These two storage objects share the same API but differ in one critical way: localStorage persists data indefinitely (until explicitly cleared), while sessionStorage keeps data only for the duration of the page session (until the tab is closed). Together, they cover the two most common client-side storage scenarios: remembering user preferences across visits and keeping temporary state within a single browsing session.
This guide covers the complete Web Storage API, the differences between localStorage and sessionStorage, how to handle the string-only limitation with JSON serialization, how to use the storage event for cross-tab communication, and the storage limits you need to be aware of.
Web Storage API: setItem, getItem, removeItem, clear, key, length
Both localStorage and sessionStorage implement the exact same Storage interface. Every method and property described here works identically on both objects.
setItem(key, value): Storing Data
Stores a key-value pair. If the key already exists, the value is overwritten:
localStorage.setItem("username", "Alice");
localStorage.setItem("theme", "dark");
localStorage.setItem("fontSize", "16");
// Overwrite an existing key
localStorage.setItem("theme", "light"); // Replaces "dark" with "light"
getItem(key): Retrieving Data
Returns the value associated with the key, or null if the key does not exist:
let username = localStorage.getItem("username");
console.log(username); // "Alice"
let theme = localStorage.getItem("theme");
console.log(theme); // "light"
// Non-existent key returns null (not undefined)
let missing = localStorage.getItem("nonexistent");
console.log(missing); // null
removeItem(key): Deleting a Single Item
Removes the key-value pair. Does nothing if the key does not exist (no error):
localStorage.removeItem("fontSize");
console.log(localStorage.getItem("fontSize")); // null
// Removing a non-existent key is harmless
localStorage.removeItem("neverExisted"); // No error
clear(): Deleting Everything
Removes all key-value pairs from the storage:
localStorage.setItem("a", "1");
localStorage.setItem("b", "2");
localStorage.setItem("c", "3");
console.log(localStorage.length); // 3
localStorage.clear();
console.log(localStorage.length); // 0
console.log(localStorage.getItem("a")); // null
clear() removes all data for the current origin, not just data your code created. If multiple scripts or libraries use localStorage on the same origin, clear() wipes everything. Use removeItem for selective deletion.
key(index): Accessing Keys by Index
Returns the name of the key at the given index. The order of keys is not guaranteed to be consistent across browsers, but the index is valid from 0 to length - 1:
localStorage.setItem("name", "Alice");
localStorage.setItem("age", "30");
localStorage.setItem("city", "Paris");
console.log(localStorage.key(0)); // e.g., "name" (order not guaranteed)
console.log(localStorage.key(1)); // e.g., "age"
console.log(localStorage.key(2)); // e.g., "city"
console.log(localStorage.key(3)); // null (out of bounds)
length: Number of Stored Items
Returns the number of key-value pairs currently stored:
console.log(localStorage.length); // 3
localStorage.removeItem("city");
console.log(localStorage.length); // 2
localStorage.clear();
console.log(localStorage.length); // 0
Iterating Over All Items
There is no forEach method on Storage, but you can iterate using length and key:
// Method 1: Using key() and length
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
let value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}
// Method 2: Using Object.keys()
let keys = Object.keys(localStorage);
for (let key of keys) {
console.log(`${key}: ${localStorage.getItem(key)}`);
}
// Method 3: Using Object.entries()
let entries = Object.entries(localStorage);
for (let [key, value] of entries) {
console.log(`${key}: ${value}`);
}
Property-Style Access (Not Recommended)
Since localStorage and sessionStorage are regular JavaScript objects, you can access items using dot notation or bracket notation. However, this is not recommended because it can conflict with built-in properties and methods:
// These work but are NOT recommended
localStorage.username = "Alice";
console.log(localStorage.username); // "Alice"
localStorage["theme"] = "dark";
console.log(localStorage["theme"]); // "dark"
// ❌ DANGER: This shadows the built-in method!
localStorage.key = "some value";
// Now localStorage.key is the string "some value", not the method!
// localStorage.key(0) throws: TypeError: localStorage.key is not a function
// ✅ Always use the official methods
localStorage.setItem("key", "some value"); // Safe
localStorage.getItem("key"); // Safe
Always use setItem, getItem, and removeItem instead of direct property access. Direct property access can overwrite built-in Storage methods like key, getItem, setItem, removeItem, clear, and length, breaking your storage code completely.
localStorage: Persistent Across Sessions
localStorage stores data with no expiration. Data persists after the browser is closed, the computer is restarted, and even after browser updates. It is only removed when explicitly cleared by JavaScript, by the user (clearing browser data), or by the browser in response to storage pressure.
Scope
localStorage is scoped to the origin (protocol + hostname + port):
https://example.com → one localStorage
http://example.com → different localStorage (different protocol)
https://example.com:8080 → different localStorage (different port)
https://sub.example.com → different localStorage (different hostname)
All pages on the same origin share the same localStorage. A value set on https://example.com/page1 is visible on https://example.com/page2.
Persistence
// Set a value on one page visit
localStorage.setItem("visitCount", "1");
// The value persists across:
// - Page refreshes
// - Closing and reopening the tab
// - Closing and reopening the browser
// - Restarting the computer
// On a later visit, the value is still there
let count = parseInt(localStorage.getItem("visitCount") || "0", 10);
count++;
localStorage.setItem("visitCount", String(count));
console.log(`You have visited ${count} times`);
Common Use Cases
// User preferences
localStorage.setItem("theme", "dark");
localStorage.setItem("language", "en");
localStorage.setItem("sidebarCollapsed", "true");
// Draft content (auto-save)
localStorage.setItem("draft-post", JSON.stringify({
title: "My Article",
content: "Work in progress...",
savedAt: Date.now()
}));
// Onboarding state
localStorage.setItem("onboardingComplete", "true");
localStorage.setItem("lastSeenVersion", "2.1.0");
// Application state
localStorage.setItem("recentSearches", JSON.stringify(["javascript", "css", "html"]));
localStorage.setItem("favoriteItems", JSON.stringify([42, 17, 89]));
When NOT to Use localStorage
- Sensitive data (passwords, tokens, personal information):
localStorageis accessible to any JavaScript running on the same origin, including XSS attacks. UseHttpOnlycookies for authentication tokens. - Large datasets (more than a few MB): Use IndexedDB instead.
- Data that needs to be sent to the server: Use cookies (they are sent automatically).
- Data that must be synchronized across devices: Use server-side storage.
sessionStorage: Per-Tab, Per-Session
sessionStorage stores data for the duration of the page session. A page session lasts as long as the browser tab is open. When the tab is closed, the data is deleted.
Scope
sessionStorage has a narrower scope than localStorage:
- Same origin (protocol + hostname + port) like
localStorage. - But also scoped to the specific browser tab. Each tab has its own independent
sessionStorage, even for the same URL.
// Tab 1: https://example.com
sessionStorage.setItem("tabData", "Tab 1 data");
// Tab 2: https://example.com (same URL, different tab)
console.log(sessionStorage.getItem("tabData")); // null (different sessionStorage!)
// Tab 2 has its own independent storage
sessionStorage.setItem("tabData", "Tab 2 data");
Persistence Rules
// sessionStorage data survives:
// ✅ Page refreshes (F5)
// ✅ In-page navigation (clicking links within the site)
// ✅ Using the browser's back/forward buttons
// sessionStorage data is deleted:
// ❌ When the tab is closed
// ❌ When the browser is closed
// Special case: "Duplicate Tab" copies sessionStorage
// Opening a link in a new tab does NOT copy sessionStorage
Duplicating a Tab
When a user duplicates a tab (right-click tab > "Duplicate"), the new tab receives a copy of the original tab's sessionStorage at the moment of duplication. After that, the two tabs have independent sessionStorage and changes in one do not affect the other.
// Original tab
sessionStorage.setItem("counter", "5");
// User duplicates the tab
// New tab starts with counter = "5"
// But changes are independent from here on
Common Use Cases
// Form wizard state (multi-step form)
sessionStorage.setItem("wizardStep", "2");
sessionStorage.setItem("wizardData", JSON.stringify({
step1: { name: "Alice", email: "alice@example.com" },
step2: { plan: "premium" }
}));
// Temporary UI state
sessionStorage.setItem("scrollPosition", "1500");
sessionStorage.setItem("activeTab", "settings");
sessionStorage.setItem("filterExpanded", "true");
// One-time notifications (show once per session)
if (!sessionStorage.getItem("welcomeShown")) {
showWelcomeMessage();
sessionStorage.setItem("welcomeShown", "true");
}
// Temporary shopping cart (before login)
sessionStorage.setItem("tempCart", JSON.stringify([
{ id: 42, quantity: 2 },
{ id: 17, quantity: 1 }
]));
localStorage vs. sessionStorage Comparison
| Feature | localStorage | sessionStorage |
|---|---|---|
| Persistence | Until explicitly cleared | Until tab is closed |
| Scope | Origin (all tabs share) | Origin + specific tab |
| Survives page refresh | Yes | Yes |
| Survives tab close | Yes | No |
| Survives browser close | Yes | No |
| Shared between tabs | Yes | No |
| Typical capacity | ~5-10 MB | ~5-10 MB |
| API | Identical | Identical |
| Use case | Preferences, cached data | Temporary state, form wizards |
String-Only Storage: JSON Serialization
Both localStorage and sessionStorage can only store strings. If you try to store a non-string value, it is automatically converted to a string using its toString() method, which usually produces undesirable results:
// ❌ Problem: Non-string values are coerced to strings
localStorage.setItem("count", 42);
console.log(localStorage.getItem("count")); // "42" (string, not number)
console.log(typeof localStorage.getItem("count")); // "string"
localStorage.setItem("active", true);
console.log(localStorage.getItem("active")); // "true" (string, not boolean)
localStorage.setItem("user", { name: "Alice" });
console.log(localStorage.getItem("user")); // "[object Object]" (useless!)
localStorage.setItem("items", [1, 2, 3]);
console.log(localStorage.getItem("items")); // "1,2,3" (lost array structure)
The Solution: JSON.stringify and JSON.parse
Use JSON.stringify() to convert any value to a JSON string before storing, and JSON.parse() to convert it back when reading:
// Storing different data types
// Numbers
localStorage.setItem("count", JSON.stringify(42));
let count = JSON.parse(localStorage.getItem("count"));
console.log(count); // 42
console.log(typeof count); // "number"
// Booleans
localStorage.setItem("darkMode", JSON.stringify(true));
let darkMode = JSON.parse(localStorage.getItem("darkMode"));
console.log(darkMode); // true
console.log(typeof darkMode); // "boolean"
// Arrays
localStorage.setItem("favorites", JSON.stringify([1, 2, 3, 4, 5]));
let favorites = JSON.parse(localStorage.getItem("favorites"));
console.log(favorites); // [1, 2, 3, 4, 5]
console.log(Array.isArray(favorites)); // true
// Objects
localStorage.setItem("user", JSON.stringify({
name: "Alice",
age: 30,
preferences: { theme: "dark", notifications: true }
}));
let user = JSON.parse(localStorage.getItem("user"));
console.log(user.name); // "Alice"
console.log(user.preferences.theme); // "dark"
// null
localStorage.setItem("data", JSON.stringify(null));
let data = JSON.parse(localStorage.getItem("data"));
console.log(data); // null
Handling Missing Keys Safely
getItem returns null for missing keys, and JSON.parse(null) returns null. But if the stored value is somehow invalid JSON, JSON.parse throws an error. Always handle this:
function getStoredJSON(key, defaultValue = null) {
let raw = localStorage.getItem(key);
if (raw === null) {
return defaultValue;
}
try {
return JSON.parse(raw);
} catch (error) {
console.warn(`Invalid JSON in localStorage for key "${key}":`, raw);
return defaultValue;
}
}
// Usage
let settings = getStoredJSON("settings", { theme: "light", fontSize: 14 });
console.log(settings); // Returns stored value or default
let visits = getStoredJSON("visitCount", 0);
console.log(visits); // Returns stored number or 0
Building a Type-Safe Storage Wrapper
const Storage = {
get(key, defaultValue = null) {
try {
let raw = localStorage.getItem(key);
return raw === null ? defaultValue : JSON.parse(raw);
} catch {
return defaultValue;
}
},
set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
},
remove(key) {
localStorage.removeItem(key);
},
has(key) {
return localStorage.getItem(key) !== null;
},
clear() {
localStorage.clear();
},
// Get all stored data as an object
getAll() {
let result = {};
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
result[key] = this.get(key);
}
return result;
},
// Get total storage used (approximate)
getSize() {
let total = 0;
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
let value = localStorage.getItem(key);
// Each character is approximately 2 bytes in JavaScript (UTF-16)
total += (key.length + value.length) * 2;
}
return total;
}
};
// Usage
Storage.set("user", { name: "Alice", age: 30 });
Storage.set("scores", [100, 95, 87, 92]);
Storage.set("darkMode", true);
Storage.set("lastLogin", Date.now());
console.log(Storage.get("user")); // { name: "Alice", age: 30 }
console.log(Storage.get("scores")); // [100, 95, 87, 92]
console.log(Storage.get("darkMode")); // true (boolean, not string)
console.log(Storage.get("missing", 0)); // 0 (default value)
console.log(`Storage used: ${(Storage.getSize() / 1024).toFixed(1)} KB`);
Storing Dates
Dates require special attention because JSON.parse does not automatically restore Date objects:
// Storing a date
let now = new Date();
localStorage.setItem("lastVisit", JSON.stringify(now));
// Stored as: "\"2024-03-15T14:30:00.000Z\"" (ISO string)
// Reading it back
let raw = JSON.parse(localStorage.getItem("lastVisit"));
console.log(typeof raw); // "string" (NOT a Date object!)
// Convert back to Date
let lastVisit = new Date(raw);
console.log(lastVisit instanceof Date); // true
console.log(lastVisit.toLocaleDateString()); // "3/15/2024"
// Alternative: store as timestamp (simpler)
localStorage.setItem("lastVisit", JSON.stringify(Date.now()));
let timestamp = JSON.parse(localStorage.getItem("lastVisit"));
let date = new Date(timestamp);
What JSON Cannot Store
JSON.stringify cannot handle certain value types:
// These are LOST during JSON serialization
// Functions
let obj = { greet: function() { return "hi"; } };
console.log(JSON.stringify(obj)); // "{}" (function is omitted)
// undefined
let obj2 = { a: 1, b: undefined };
console.log(JSON.stringify(obj2)); // '{"a":1}' (undefined is omitted)
// Symbols
let obj3 = { [Symbol("id")]: 42 };
console.log(JSON.stringify(obj3)); // "{}" (symbols are omitted)
// Circular references
let circular = {};
circular.self = circular;
// JSON.stringify(circular); // TypeError: Converting circular structure to JSON
// Map, Set, RegExp: serialized as empty objects
console.log(JSON.stringify(new Map([["a", 1]]))); // "{}"
console.log(JSON.stringify(new Set([1, 2, 3]))); // "{}"
console.log(JSON.stringify(/pattern/)); // "{}"
If you need to store Map, Set, or other complex types, convert them first:
// Map
let map = new Map([["name", "Alice"], ["age", 30]]);
Storage.set("myMap", Array.from(map.entries()));
let restoredMap = new Map(Storage.get("myMap"));
// Set
let set = new Set([1, 2, 3, 4, 5]);
Storage.set("mySet", Array.from(set));
let restoredSet = new Set(Storage.get("mySet"));
The storage Event (Cross-Tab Communication)
When localStorage is modified in one tab, all other tabs on the same origin receive a storage event. This event does not fire in the tab that made the change. This makes localStorage a simple mechanism for cross-tab communication.
Basic Usage
// Tab A: Listen for changes from other tabs
window.addEventListener("storage", (event) => {
console.log("Storage changed in another tab!");
console.log("Key:", event.key); // The key that was changed
console.log("Old value:", event.oldValue); // Previous value (or null)
console.log("New value:", event.newValue); // New value (or null if removed)
console.log("URL:", event.url); // URL of the page that made the change
console.log("Storage area:", event.storageArea); // The localStorage object
});
// Tab B: Make a change
localStorage.setItem("theme", "dark");
// Tab A receives the storage event with:
// key: "theme", oldValue: "light", newValue: "dark"
Event Properties
| Property | Description |
|---|---|
key | The key that was changed. null if clear() was called. |
oldValue | The previous value. null if the key was newly created. |
newValue | The new value. null if the key was removed. |
url | The URL of the document that made the change. |
storageArea | The Storage object (localStorage or sessionStorage). |
Detecting Different Types of Changes
window.addEventListener("storage", (event) => {
if (event.key === null) {
// clear() was called: all data was removed
console.log("All storage was cleared!");
return;
}
if (event.oldValue === null) {
// New key was created
console.log(`New key added: ${event.key} = ${event.newValue}`);
} else if (event.newValue === null) {
// Key was removed
console.log(`Key removed: ${event.key} (was: ${event.oldValue})`);
} else {
// Key was updated
console.log(`Key updated: ${event.key}: ${event.oldValue} → ${event.newValue}`);
}
});
Practical Example: Cross-Tab Theme Sync
// Theme manager that syncs across tabs
function initThemeSync() {
// Apply the current theme
let theme = localStorage.getItem("theme") || "light";
applyTheme(theme);
// Listen for theme changes from other tabs
window.addEventListener("storage", (event) => {
if (event.key === "theme" && event.newValue) {
applyTheme(event.newValue);
console.log(`Theme changed in another tab: ${event.newValue}`);
}
});
// Theme toggle button
document.getElementById("themeToggle")?.addEventListener("click", () => {
let current = localStorage.getItem("theme") || "light";
let newTheme = current === "light" ? "dark" : "light";
localStorage.setItem("theme", newTheme);
applyTheme(newTheme); // Apply locally (storage event won't fire in this tab)
});
}
function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
}
initThemeSync();
Practical Example: Cross-Tab Logout
// When one tab logs out, all tabs should log out
window.addEventListener("storage", (event) => {
if (event.key === "authToken" && event.newValue === null) {
// Token was removed: user logged out in another tab
console.log("Logged out from another tab");
window.location.href = "/login";
}
if (event.key === "authToken" && event.oldValue === null && event.newValue) {
// User logged in from another tab
console.log("Logged in from another tab");
window.location.reload();
}
});
function logout() {
localStorage.removeItem("authToken");
localStorage.removeItem("user");
// Other tabs will detect this and redirect to login
window.location.href = "/login";
}
Practical Example: Simple Cross-Tab Messaging
You can use localStorage as a simple messaging channel between tabs:
// Send a message to other tabs
function broadcastMessage(type, data) {
let message = JSON.stringify({
type,
data,
timestamp: Date.now(),
tabId: getTabId()
});
// Write and immediately remove to trigger the event without leaving data
localStorage.setItem("__broadcast__", message);
localStorage.removeItem("__broadcast__");
}
// Receive messages from other tabs
window.addEventListener("storage", (event) => {
if (event.key !== "__broadcast__" || !event.newValue) return;
try {
let message = JSON.parse(event.newValue);
handleBroadcast(message);
} catch (e) {
// Invalid message, ignore
}
});
function handleBroadcast(message) {
switch (message.type) {
case "CART_UPDATED":
console.log("Cart updated in another tab:", message.data);
refreshCart();
break;
case "USER_UPDATED":
console.log("User profile updated:", message.data);
refreshUserInfo();
break;
case "NOTIFICATION":
showNotification(message.data.text);
break;
}
}
// Unique tab identifier
function getTabId() {
if (!sessionStorage.getItem("__tabId__")) {
sessionStorage.setItem("__tabId__", Math.random().toString(36).substr(2, 9));
}
return sessionStorage.getItem("__tabId__");
}
// Usage
broadcastMessage("CART_UPDATED", { itemCount: 3, total: 59.99 });
broadcastMessage("NOTIFICATION", { text: "Your order has shipped!" });
The storage event only fires on localStorage changes, not sessionStorage changes. Since sessionStorage is tab-specific, there are no other tabs to notify. For cross-tab communication, always use localStorage.
For more advanced cross-tab communication, consider the BroadcastChannel API, which was designed specifically for this purpose and does not rely on storage side-effects.
Storage Limits and Quotas
How Much Can You Store?
The Web Storage specification recommends a minimum of 5 MB per origin, but actual limits vary by browser:
| Browser | localStorage Limit | sessionStorage Limit |
|---|---|---|
| Chrome | ~5 MB | ~5 MB |
| Firefox | ~5-10 MB | ~5-10 MB |
| Safari | ~5 MB | ~5 MB |
| Edge | ~5 MB | ~5 MB |
These limits are for the total size of all key-value pairs combined (keys + values), measured in UTF-16 characters (2 bytes per character).
Detecting Available Space
There is no built-in API to check how much storage space is available. You can estimate current usage:
function getStorageUsage(storage = localStorage) {
let totalBytes = 0;
for (let i = 0; i < storage.length; i++) {
let key = storage.key(i);
let value = storage.getItem(key);
// UTF-16: each character is 2 bytes
totalBytes += (key.length + value.length) * 2;
}
return {
bytes: totalBytes,
kilobytes: (totalBytes / 1024).toFixed(2),
megabytes: (totalBytes / 1024 / 1024).toFixed(2),
items: storage.length
};
}
let usage = getStorageUsage();
console.log(`Using ${usage.kilobytes} KB across ${usage.items} items`);
Testing the Limit
You can experimentally determine the storage limit:
function testStorageLimit() {
let testKey = "__storage_test__";
let chunk = "x".repeat(1024); // 1 KB of data
let totalStored = 0;
try {
// Clear previous test data
localStorage.removeItem(testKey);
let data = "";
while (true) {
data += chunk;
localStorage.setItem(testKey, data);
totalStored = data.length;
}
} catch (e) {
console.log(`Storage limit reached at approximately ${(totalStored * 2 / 1024 / 1024).toFixed(2)} MB`);
} finally {
localStorage.removeItem(testKey);
}
}
Handling the Quota Exceeded Error
When storage is full, setItem throws a QuotaExceededError (a DOMException). Always handle this:
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (error) {
if (error.name === "QuotaExceededError" ||
error.code === 22 || // Legacy browsers
error.code === 1014) { // Firefox
console.warn("localStorage is full!");
// Strategy: try to make room
let freed = cleanupOldData();
if (freed) {
try {
localStorage.setItem(key, value);
return true;
} catch {
console.error("Still not enough space after cleanup");
return false;
}
}
return false;
}
throw error; // Re-throw if it's a different error
}
}
function cleanupOldData() {
// Remove items with timestamps older than 30 days
let cutoff = Date.now() - 30 * 24 * 60 * 60 * 1000;
let removed = 0;
for (let i = localStorage.length - 1; i >= 0; i--) {
let key = localStorage.key(i);
if (key.startsWith("cache_")) {
try {
let data = JSON.parse(localStorage.getItem(key));
if (data.timestamp && data.timestamp < cutoff) {
localStorage.removeItem(key);
removed++;
}
} catch {
// Invalid data, remove it
localStorage.removeItem(key);
removed++;
}
}
}
console.log(`Cleaned up ${removed} old items`);
return removed > 0;
}
Storage Availability Detection
In some browsers, storage may be unavailable (private browsing mode, storage disabled, iframe restrictions):
function isStorageAvailable(type = "localStorage") {
try {
let storage = window[type];
let testKey = "__storage_test__";
storage.setItem(testKey, "test");
storage.removeItem(testKey);
return true;
} catch (e) {
return false;
}
}
if (isStorageAvailable("localStorage")) {
console.log("localStorage is available");
} else {
console.log("localStorage is NOT available, using fallback");
// Fall back to in-memory storage or cookies
}
if (isStorageAvailable("sessionStorage")) {
console.log("sessionStorage is available");
}
In-Memory Fallback
For environments where localStorage is unavailable, provide a fallback:
function createStorage() {
if (isStorageAvailable("localStorage")) {
return localStorage;
}
// In-memory fallback
console.warn("localStorage unavailable, using in-memory fallback");
let store = new Map();
return {
getItem(key) {
return store.has(key) ? store.get(key) : null;
},
setItem(key, value) {
store.set(key, String(value));
},
removeItem(key) {
store.delete(key);
},
clear() {
store.clear();
},
key(index) {
return [...store.keys()][index] ?? null;
},
get length() {
return store.size;
}
};
}
let storage = createStorage();
// Use `storage` everywhere instead of `localStorage` directly
storage.setItem("test", "works");
console.log(storage.getItem("test")); // "works"
Practical Example: Settings Manager
Here is a comprehensive example that combines everything: type-safe storage, sessionStorage for temporary state, localStorage for persistent preferences, and cross-tab synchronization:
<!DOCTYPE html>
<html>
<head>
<title>Settings Manager</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
body[data-theme="dark"] { background: #1a1a2e; color: #e0e0e0; }
.card { background: rgba(128,128,128,0.1); padding: 20px; border-radius: 12px; margin: 16px 0; }
.card h3 { margin-bottom: 12px; }
.setting { display: flex; justify-content: space-between; align-items: center;
padding: 10px 0; border-bottom: 1px solid rgba(128,128,128,0.2); }
.setting:last-child { border-bottom: none; }
select, input[type="range"] { padding: 4px; }
button { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer;
background: #2196f3; color: white; margin: 4px; }
button:hover { opacity: 0.9; }
button.danger { background: #f44336; }
.storage-info { font-family: monospace; font-size: 13px; line-height: 1.8; }
.tab-id { font-family: monospace; background: rgba(128,128,128,0.2);
padding: 2px 8px; border-radius: 4px; }
.log { max-height: 150px; overflow-y: auto; font-family: monospace;
font-size: 12px; background: rgba(0,0,0,0.1); padding: 10px;
border-radius: 6px; line-height: 1.6; }
</style>
</head>
<body>
<h1>Settings Manager</h1>
<p>Tab ID: <span class="tab-id" id="tabId"></span>
(Open this page in multiple tabs to see sync)</p>
<div class="card">
<h3>Persistent Settings (localStorage)</h3>
<div class="setting">
<label>Theme</label>
<select id="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="setting">
<label>Font Size: <span id="fontSizeValue">16</span>px</label>
<input type="range" id="fontSize" min="12" max="24" value="16">
</div>
<div class="setting">
<label>Language</label>
<select id="language">
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
</select>
</div>
</div>
<div class="card">
<h3>Session State (sessionStorage)</h3>
<div class="setting">
<label>Session note</label>
<input type="text" id="sessionNote" placeholder="This tab only...">
</div>
<p style="font-size: 13px; color: #888; margin-top: 8px;">
This value is unique to this tab and lost when the tab closes.
</p>
</div>
<div class="card">
<h3>Storage Info</h3>
<div class="storage-info" id="storageInfo"></div>
<div style="margin-top: 12px;">
<button onclick="exportSettings()">Export Settings</button>
<button class="danger" onclick="clearAllSettings()">Clear All</button>
</div>
</div>
<div class="card">
<h3>Sync Log</h3>
<div class="log" id="syncLog">Waiting for changes from other tabs...</div>
</div>
<script>
// --- Storage Wrapper ---
const Store = {
get(key, defaultValue = null) {
try {
let raw = localStorage.getItem(key);
return raw === null ? defaultValue : JSON.parse(raw);
} catch { return defaultValue; }
},
set(key, value) {
try { localStorage.setItem(key, JSON.stringify(value)); }
catch (e) { console.error("Storage full:", e); }
}
};
// --- Tab ID ---
if (!sessionStorage.getItem("tabId")) {
sessionStorage.setItem("tabId", Math.random().toString(36).substr(2, 6));
}
document.getElementById("tabId").textContent = sessionStorage.getItem("tabId");
// --- Load Settings ---
function loadSettings() {
let theme = Store.get("theme", "light");
let fontSize = Store.get("fontSize", 16);
let language = Store.get("language", "en");
document.getElementById("theme").value = theme;
document.getElementById("fontSize").value = fontSize;
document.getElementById("fontSizeValue").textContent = fontSize;
document.getElementById("language").value = language;
applySettings(theme, fontSize);
// Session state
let note = sessionStorage.getItem("sessionNote") || "";
document.getElementById("sessionNote").value = note;
updateStorageInfo();
}
function applySettings(theme, fontSize) {
document.body.setAttribute("data-theme", theme);
document.body.style.fontSize = fontSize + "px";
}
// --- Setting Change Handlers ---
document.getElementById("theme").addEventListener("change", (e) => {
Store.set("theme", e.target.value);
applySettings(e.target.value, Store.get("fontSize", 16));
updateStorageInfo();
});
document.getElementById("fontSize").addEventListener("input", (e) => {
let size = parseInt(e.target.value);
document.getElementById("fontSizeValue").textContent = size;
Store.set("fontSize", size);
applySettings(Store.get("theme", "light"), size);
updateStorageInfo();
});
document.getElementById("language").addEventListener("change", (e) => {
Store.set("language", e.target.value);
updateStorageInfo();
});
document.getElementById("sessionNote").addEventListener("input", (e) => {
sessionStorage.setItem("sessionNote", e.target.value);
updateStorageInfo();
});
// --- Cross-Tab Sync ---
window.addEventListener("storage", (event) => {
let log = document.getElementById("syncLog");
let time = new Date().toLocaleTimeString();
if (event.key === null) {
log.textContent += `\n[${time}] All storage cleared from another tab`;
loadSettings();
return;
}
log.textContent += `\n[${time}] "${event.key}" changed: ${event.oldValue} → ${event.newValue}`;
log.scrollTop = log.scrollHeight;
// Reload affected settings
if (["theme", "fontSize", "language"].includes(event.key)) {
loadSettings();
}
});
// --- Storage Info ---
function updateStorageInfo() {
let localSize = 0;
for (let i = 0; i < localStorage.length; i++) {
let k = localStorage.key(i);
localSize += (k.length + localStorage.getItem(k).length) * 2;
}
let sessionSize = 0;
for (let i = 0; i < sessionStorage.length; i++) {
let k = sessionStorage.key(i);
sessionSize += (k.length + sessionStorage.getItem(k).length) * 2;
}
document.getElementById("storageInfo").innerHTML = `
`;
}
// --- Export/Clear ---
function exportSettings() {
let settings = {};
for (let i = 0; i < localStorage.length; i++) {
let key = localStorage.key(i);
settings[key] = Store.get(key);
}
let blob = new Blob([JSON.stringify(settings, null, 2)], { type: "application/json" });
let url = URL.createObjectURL(blob);
let a = document.createElement("a");
a.href = url;
a.download = "settings.json";
a.click();
URL.revokeObjectURL(url);
}
function clearAllSettings() {
if (confirm("Clear all stored settings?")) {
localStorage.clear();
sessionStorage.clear();
loadSettings();
}
}
// --- Initialize ---
loadSettings();
</script>
</body>
</html>
This example demonstrates:
localStoragefor persistent settings (theme, font size, language) that survive browser restartssessionStoragefor tab-specific temporary data (session note, tab ID)- JSON serialization with the
Storewrapper for type-safe storage storageevent for real-time cross-tab synchronization with a visible sync log- Storage usage tracking showing bytes and items for both storage types
- Export functionality using Blob and
JSON.stringify - Error handling with try/catch on storage operations
Summary
localStorage and sessionStorage provide a simple key-value storage API for client-side data that does not need to be sent to the server.
Shared API (Storage interface):
| Method/Property | Description |
|---|---|
setItem(key, value) | Store a key-value pair (strings only) |
getItem(key) | Retrieve a value (returns null if missing) |
removeItem(key) | Delete a single key |
clear() | Delete all keys |
key(index) | Get the key name at the given index |
length | Number of stored items |
localStorage vs. sessionStorage:
localStorage | sessionStorage | |
|---|---|---|
| Persistence | Until explicitly cleared | Until tab closes |
| Tab scope | Shared across all tabs (same origin) | Unique to each tab |
Fires storage event | Yes (in other tabs) | No |
| Best for | Preferences, cached data, user settings | Temporary state, form wizards, per-tab data |
String-Only Storage:
- All values are stored as strings. Non-strings are coerced via
toString(). - Use
JSON.stringify()when storing andJSON.parse()when reading. - Always wrap
JSON.parsein try/catch to handle corrupted data. - Dates, Maps, Sets, and functions require special handling.
storage Event (Cross-Tab Communication):
- Fires on
windowwhenlocalStoragechanges in another tab (same origin). - Does not fire in the tab that made the change.
- Does not fire for
sessionStoragechanges. - Event properties:
key,oldValue,newValue,url,storageArea. - When
clear()is called,keyisnull.
Storage Limits:
- Approximately 5-10 MB per origin (varies by browser).
setItemthrowsQuotaExceededError(DOMException) when full.- Always handle quota errors with try/catch.
- Check storage availability before use (private browsing may disable it).
Best Practices:
- Always use
setItem/getItemmethods, never direct property access. - Use a wrapper with JSON serialization for type safety.
- Handle
QuotaExceededErrorgracefully. - Test for storage availability in your initialization code.
- Do not store sensitive data (accessible to any JS on the origin).
- Use
localStoragefor data that should persist. UsesessionStoragefor data that should not.