Use localStorage and sessionStorage in WordPress themes with JavaScript

The Web Storage API provides two mechanisms for storing key-value data in the browser: localStorage persists until explicitly cleared and survives browser restarts, and sessionStorage is scoped to the current browser tab and cleared when the tab is closed. Both APIs store string values synchronously, have a storage limit of approximately 5 MB per origin, and are accessed through a simple interface — setItem(), getItem(), removeItem(), and clear(). In WordPress themes they are ideal for storing lightweight user preferences that do not need to be sent to the server: dark mode toggle state, sidebar open/closed state, dismissed admin notices or cookie banners, reading position for long articles, and recently viewed post IDs for a client-side “recently viewed” widget. Because localStorage is synchronous and blocks the main thread, you should only read from it during page initialisation — reading in an event handler is fine, but avoid reading it inside scroll or animation loops. Always wrap storage reads in a try/catch block because localStorage throws a SecurityError in private browsing mode in some browsers, and a QuotaExceededError when the storage limit is reached. Storing objects requires serialising them with JSON.stringify() on write and JSON.parse() on read. For sensitive data, storage is not appropriate — use HttpOnly cookies instead, as localStorage is accessible to any JavaScript on the same origin including injected scripts. For cross-tab synchronisation, the storage event fires in other open tabs when localStorage changes, which is useful for syncing dark mode across multiple open WordPress admin tabs. Pair this with the debounce guide for scroll-position saving and the accordion guide for persisting open/closed state.

Problem: User preferences like dark mode state and dismissed notices reset on every page load because there is no mechanism to persist them between visits without server-side sessions.

Solution: Use localStorage to persist preferences and apply them immediately on page load to prevent flash of unstyled content:

/**
 * Storage helper — wraps localStorage with JSON support and error handling.
 */
const store = {
    get( key, fallback = null ) {
        try {
            const raw = localStorage.getItem( key );
            return raw !== null ? JSON.parse( raw ) : fallback;
        } catch {
            return fallback;
        }
    },
    set( key, value ) {
        try {
            localStorage.setItem( key, JSON.stringify( value ) );
        } catch { /* QuotaExceededError — fail silently */ }
    },
    remove( key ) {
        try { localStorage.removeItem( key ); } catch { }
    },
};

// Example 1: Dark mode toggle persisted across visits
const darkBtn  = document.querySelector( '#toggle-dark' );
const htmlEl   = document.documentElement;

// Apply BEFORE first paint to avoid flash (place inline in )
if ( store.get( 'darkMode', false ) ) {
    htmlEl.classList.add( 'dark-mode' );
}

darkBtn && darkBtn.addEventListener( 'click', function () {
    const isDark = htmlEl.classList.toggle( 'dark-mode' );
    store.set( 'darkMode', isDark );
    darkBtn.setAttribute( 'aria-pressed', String( isDark ) );
} );

// Example 2: Dismiss a notice and remember it for 7 days
const notice   = document.querySelector( '.ha-notice' );
const closeBtn = notice && notice.querySelector( '.ha-notice__close' );

if ( notice ) {
    const dismissed = store.get( 'noticeDismissed', 0 );
    if ( dismissed && Date.now() - dismissed < 7 * 24 * 60 * 60 * 1000 ) {
        notice.hidden = true; // Already dismissed within 7 days
    }
}

closeBtn && closeBtn.addEventListener( 'click', function () {
    notice.hidden = true;
    store.set( 'noticeDismissed', Date.now() );
} );

// Example 3: sessionStorage — remember scroll position for current session
window.addEventListener( 'beforeunload', function () {
    sessionStorage.setItem( 'scrollY', String( window.scrollY ) );
} );

window.addEventListener( 'load', function () {
    const savedY = parseInt( sessionStorage.getItem( 'scrollY' ) || '0', 10 );
    if ( savedY > 0 ) window.scrollTo( 0, savedY );
} );

NOTE: The dark mode script that reads localStorage and applies the class must run as early as possible — ideally in an inline <script> tag inside <head> before any stylesheets, to prevent a flash of the default (light) theme before the class is applied. In WordPress, use wp_head with priority 1 or output it directly in the header.php template before the </head> tag. Never store passwords, tokens, or personally identifiable information in localStorage — it is accessible to all JavaScript on the same origin, making it vulnerable to XSS attacks.