JS SessionStorage API Guide: Complete Tutorial
A complete tutorial on the JavaScript SessionStorage API. Covers getItem, setItem, removeItem, clear, tab-scoped persistence, sessionStorage vs localStorage differences, form state preservation, multi-step wizard tracking, session-scoped caching, storage event behavior, and building a session state manager.
SessionStorage provides tab-scoped key-value storage that persists across page reloads within the same tab but clears when the tab closes. It shares the same API as localStorage but with different lifetime semantics. This guide covers every method, use case, and cross-tab behavior.
SessionStorage vs LocalStorage
| Feature | sessionStorage | localStorage |
|---|---|---|
| Lifetime | Until tab closes | Until explicitly cleared |
| Scope | Per tab per origin | Per origin (all tabs) |
| Storage limit | ~5MB | ~5MB |
| Survives page reload | Yes | Yes |
| Survives tab close | No | Yes |
| Survives browser restart | No | Yes |
| Fires storage event cross-tab | No | Yes |
| API | Identical | Identical |
Basic Operations
// Store a value
sessionStorage.setItem("currentStep", "3");
// Retrieve a value
const step = sessionStorage.getItem("currentStep"); // "3"
// Remove a specific key
sessionStorage.removeItem("currentStep");
// Check existence
if (sessionStorage.getItem("sessionId") !== null) {
console.log("Session exists");
}
// Clear all session storage for this origin/tab
sessionStorage.clear();
// Count stored keys
console.log(`${sessionStorage.length} items in sessionStorage`);For the full localStorage API reference, see JS localStorage API guide: a complete tutorial.
Form State Preservation
Prevent data loss when users accidentally refresh the page during form entry:
class FormStatePreserver {
constructor(formId) {
this.formId = formId;
this.storageKey = `form:${formId}`;
this.form = document.getElementById(formId);
this.debounceTimer = null;
}
init() {
// Restore saved state
this.restore();
// Save on every input change (debounced)
this.form.addEventListener("input", () => {
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(() => this.save(), 300);
});
// Clear on successful submit
this.form.addEventListener("submit", () => {
this.clear();
});
}
save() {
const data = {};
const inputs = this.form.querySelectorAll("input, textarea, select");
inputs.forEach((input) => {
if (!input.name) return;
if (input.type === "checkbox") {
data[input.name] = input.checked;
} else if (input.type === "radio") {
if (input.checked) data[input.name] = input.value;
} else if (input.type !== "password" && input.type !== "file") {
data[input.name] = input.value;
}
});
sessionStorage.setItem(this.storageKey, JSON.stringify(data));
}
restore() {
const raw = sessionStorage.getItem(this.storageKey);
if (!raw) return;
try {
const data = JSON.parse(raw);
const inputs = this.form.querySelectorAll("input, textarea, select");
inputs.forEach((input) => {
if (!input.name || !(input.name in data)) return;
if (input.type === "checkbox") {
input.checked = data[input.name];
} else if (input.type === "radio") {
input.checked = input.value === data[input.name];
} else {
input.value = data[input.name];
}
});
} catch {
// Corrupted data, clear it
this.clear();
}
}
clear() {
sessionStorage.removeItem(this.storageKey);
}
}
// Usage
const preserver = new FormStatePreserver("registration-form");
preserver.init();Multi-Step Wizard Tracker
class WizardTracker {
constructor(wizardName, totalSteps) {
this.key = `wizard:${wizardName}`;
this.totalSteps = totalSteps;
}
getState() {
const raw = sessionStorage.getItem(this.key);
if (!raw) {
return {
currentStep: 1,
completedSteps: [],
data: {},
startedAt: new Date().toISOString(),
};
}
return JSON.parse(raw);
}
saveStepData(step, data) {
const state = this.getState();
state.data[`step${step}`] = data;
if (!state.completedSteps.includes(step)) {
state.completedSteps.push(step);
state.completedSteps.sort((a, b) => a - b);
}
state.currentStep = Math.min(step + 1, this.totalSteps);
sessionStorage.setItem(this.key, JSON.stringify(state));
}
goToStep(step) {
const state = this.getState();
state.currentStep = step;
sessionStorage.setItem(this.key, JSON.stringify(state));
}
getStepData(step) {
return this.getState().data[`step${step}`] || null;
}
isStepCompleted(step) {
return this.getState().completedSteps.includes(step);
}
getProgress() {
const state = this.getState();
return {
current: state.currentStep,
completed: state.completedSteps.length,
total: this.totalSteps,
percentage: Math.round(
(state.completedSteps.length / this.totalSteps) * 100
),
};
}
getAllData() {
const state = this.getState();
return Object.values(state.data).reduce(
(merged, stepData) => ({ ...merged, ...stepData }),
{}
);
}
reset() {
sessionStorage.removeItem(this.key);
}
}
// Usage
const wizard = new WizardTracker("checkout", 4);
// Step 1: shipping info
wizard.saveStepData(1, {
address: "123 Main St",
city: "Austin",
zip: "78701",
});
// Step 2: payment
wizard.saveStepData(2, {
cardLast4: "4242",
method: "credit",
});
console.log(wizard.getProgress());
// { current: 3, completed: 2, total: 4, percentage: 50 }
// On final submit
const allData = wizard.getAllData();
console.log(allData);
// { address: "123 Main St", city: "Austin", zip: "78701", cardLast4: "4242", method: "credit" }
wizard.reset();Session-Scoped Caching
class SessionCache {
constructor(prefix = "cache") {
this.prefix = prefix;
}
async fetchWithCache(url, options = {}) {
const cacheKey = `${this.prefix}:${url}`;
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
try {
const entry = JSON.parse(cached);
console.log(`Cache hit: ${url}`);
return entry.data;
} catch {
sessionStorage.removeItem(cacheKey);
}
}
console.log(`Cache miss: ${url}`);
const response = await fetch(url, options);
const data = await response.json();
try {
sessionStorage.setItem(
cacheKey,
JSON.stringify({ data, cachedAt: Date.now() })
);
} catch (error) {
if (error.name === "QuotaExceededError") {
this.evictOldest();
}
}
return data;
}
evictOldest() {
let oldest = { key: null, time: Infinity };
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (!key.startsWith(this.prefix)) continue;
try {
const entry = JSON.parse(sessionStorage.getItem(key));
if (entry.cachedAt < oldest.time) {
oldest = { key, time: entry.cachedAt };
}
} catch {
// Remove corrupted entries
sessionStorage.removeItem(key);
}
}
if (oldest.key) {
sessionStorage.removeItem(oldest.key);
}
}
invalidate(url) {
sessionStorage.removeItem(`${this.prefix}:${url}`);
}
clearAll() {
const keys = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key.startsWith(this.prefix)) keys.push(key);
}
keys.forEach((k) => sessionStorage.removeItem(k));
}
}
// Usage
const cache = new SessionCache("api");
const users = await cache.fetchWithCache("/api/users");
const posts = await cache.fetchWithCache("/api/posts");
// Second call uses cache
const usersAgain = await cache.fetchWithCache("/api/users"); // "Cache hit"Tab Navigation History
class TabHistory {
constructor() {
this.key = "tab:history";
}
record(page) {
const history = this.getHistory();
history.push({
page,
timestamp: Date.now(),
title: document.title,
});
// Keep last 50 entries
if (history.length > 50) {
history.splice(0, history.length - 50);
}
sessionStorage.setItem(this.key, JSON.stringify(history));
}
getHistory() {
const raw = sessionStorage.getItem(this.key);
return raw ? JSON.parse(raw) : [];
}
getLastPage() {
const history = this.getHistory();
return history.length > 1 ? history[history.length - 2] : null;
}
getPageCount(page) {
return this.getHistory().filter((entry) => entry.page === page).length;
}
getSessionDuration() {
const history = this.getHistory();
if (history.length < 2) return 0;
return history[history.length - 1].timestamp - history[0].timestamp;
}
}
// Record current page
const tabHistory = new TabHistory();
tabHistory.record(window.location.pathname);Per-Tab Session Behavior
| Scenario | sessionStorage behavior |
|---|---|
| Page reload (F5) | Data persists |
| Navigate to new URL on same origin | Data persists |
| Open link in same tab | Data persists |
| Open link in new tab (Ctrl+Click) | New tab gets a COPY of sessionStorage |
| window.open() | New window gets a COPY |
| Duplicate tab | Gets a COPY at duplication time |
| Close tab and reopen | Data is GONE |
| Browser crash recovery | Behavior varies by browser |
// Detect if this is a duplicated tab
function isOriginalTab() {
const tabId = sessionStorage.getItem("tabId");
if (!tabId) {
sessionStorage.setItem("tabId", crypto.randomUUID());
return true;
}
// On duplicate, tabId exists immediately
// Generate a new one for this tab
const newId = crypto.randomUUID();
sessionStorage.setItem("tabId", newId);
return false;
}
if (!isOriginalTab()) {
console.log("This tab was duplicated from another tab");
}Feature Detection
function isSessionStorageAvailable() {
try {
const testKey = "__session_test__";
sessionStorage.setItem(testKey, "1");
sessionStorage.removeItem(testKey);
return true;
} catch {
return false;
}
}
// Fallback for environments without sessionStorage
class MemoryStorage {
constructor() {
this.store = new Map();
}
getItem(key) {
return this.store.get(key) ?? null;
}
setItem(key, value) {
this.store.set(key, String(value));
}
removeItem(key) {
this.store.delete(key);
}
clear() {
this.store.clear();
}
key(index) {
return [...this.store.keys()][index] ?? null;
}
get length() {
return this.store.size;
}
}
const storage = isSessionStorageAvailable()
? sessionStorage
: new MemoryStorage();Rune AI
Key Insights
- Tab-scoped lifetime: Data persists across reloads but clears when the tab closes, making it ideal for transient session data
- Same API as localStorage: Use
setItem,getItem,removeItem,clearidentically; only the persistence behavior differs - Form state preservation: Save form inputs on every change (debounced) and restore on page reload to prevent data loss
- Duplicated tabs get copies: Opening a link in a new tab or duplicating a tab copies sessionStorage at that moment; changes diverge afterward
- No reliable cross-tab events: The
storageevent is unreliable for sessionStorage across tabs; use localStorage or BroadcastChannel for cross-tab communication
Frequently Asked Questions
When should I use sessionStorage instead of localStorage?
Does sessionStorage work across iframes?
Can I store objects in sessionStorage?
Does the storage event fire for sessionStorage changes?
What happens to sessionStorage when a page redirects?
Conclusion
SessionStorage provides tab-scoped, reload-safe temporary storage ideal for form preservation, wizard tracking, session caching, and navigation history. It shares localStorage's API but automatically cleans up when the tab closes. For persistent storage needs, see JS localStorage API guide: a complete tutorial. For cookie-based storage, see how to manage cookies in JS complete tutorial.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.