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.

JavaScriptintermediate
14 min read

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

FeaturesessionStoragelocalStorage
LifetimeUntil tab closesUntil explicitly cleared
ScopePer tab per originPer origin (all tabs)
Storage limit~5MB~5MB
Survives page reloadYesYes
Survives tab closeNoYes
Survives browser restartNoYes
Fires storage event cross-tabNoYes
APIIdenticalIdentical

Basic Operations

javascriptjavascript
// 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:

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
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

ScenariosessionStorage behavior
Page reload (F5)Data persists
Navigate to new URL on same originData persists
Open link in same tabData persists
Open link in new tab (Ctrl+Click)New tab gets a COPY of sessionStorage
window.open()New window gets a COPY
Duplicate tabGets a COPY at duplication time
Close tab and reopenData is GONE
Browser crash recoveryBehavior varies by browser
javascriptjavascript
// 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

javascriptjavascript
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

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, clear identically; 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 storage event is unreliable for sessionStorage across tabs; use localStorage or BroadcastChannel for cross-tab communication
RunePowered by Rune AI

Frequently Asked Questions

When should I use sessionStorage instead of localStorage?

Use sessionStorage for temporary data that should not persist after the user closes the tab: form drafts, wizard progress, one-time notifications, navigation breadcrumbs, and session-scoped API caches. Use localStorage for data that should survive across sessions. See [JS localStorage API guide: a complete tutorial](/tutorials/programming-languages/javascript/js-localstorage-api-guide-a-complete-tutorial) for persistent storage.

Does sessionStorage work across iframes?

Yes, if the iframe is on the same origin. Each iframe on the same origin shares the same sessionStorage as the parent window for that tab. Cross-origin iframes have their own separate sessionStorage.

Can I store objects in sessionStorage?

Yes, using `JSON.stringify` and `JSON.parse`, exactly like localStorage. For advanced serialization with Dates, Maps, and Sets, see [storing complex objects in JS localStorage guide](/tutorials/programming-languages/javascript/storing-complex-objects-in-js-localstorage-guide). The same patterns apply to sessionStorage.

Does the storage event fire for sessionStorage changes?

The specification says `storage` events should fire for sessionStorage changes in other browsing contexts that share the same session. In practice, because each tab has its own sessionStorage, the event rarely fires. Do not rely on it for cross-tab communication; use localStorage or the BroadcastChannel API instead.

What happens to sessionStorage when a page redirects?

If the redirect stays within the same origin and the same tab, sessionStorage persists. Cross-origin redirects create a new sessionStorage context for the new origin. The original origin's sessionStorage remains intact for future navigations back.

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.