You have a splash screen or an intro animation on your site. It shows once when the user lands. Perfect. Then the user opens your blog in a new tab — and the splash plays again. They open another page — splash again.
This is not a bug in your logic. It is a fundamental property of sessionStorage that catches almost every developer off guard the first time.
What sessionStorage Actually Means
The MDN definition says sessionStorage data is cleared when the browser session ends. Most developers read that and assume it means something like a user login session. It does not.
The browser session in the Web Storage specification means exactly one browser tab or window. Each tab has its own completely isolated sessionStorage context. A value you write in Tab 1 is invisible to Tab 2, Tab 3, or any other tab — even if they are on the exact same page.
This is by design. sessionStorage was built for forms and wizard-style flows where you need per-tab isolation. But it is the wrong tool when you need shared state across all tabs of the same origin.
Why the Splash Screen Breaks
Here is what happens with a naive implementation:
- › User opens site in Tab 1 — sessionStorage is empty — splash shows
- › User clicks to enter — code writes storm_entered = "1" to sessionStorage of Tab 1
- › User opens your blog in a new tab (Tab 2) — Tab 2 has its own fresh, empty sessionStorage
- › Code checks sessionStorage for storm_entered — it is not there — splash shows again
The flag was written correctly. The check is correct. But sessionStorage is per-tab, so Tab 2 never sees what Tab 1 wrote.
Option A — localStorage with Tab-Count Handshake (Recommended)
This is the approach that matches the exact behavior most sites need: show the splash once while any tab is open, reset when all tabs close.
The mechanism uses three keys:
- › storm_tab_id in sessionStorage — a unique UUID for this specific tab, cleared automatically when the tab closes
- › storm_open_tabs in localStorage — a JSON array of all currently active tab IDs, shared across all tabs
- › storm_splash_shown in localStorage — a boolean flag marking that the splash was dismissed this session, also shared
How it works step by step:
- › Tab opens — if no storm_tab_id in sessionStorage, this is a fresh tab
- › Generate a UUID, write it to sessionStorage.storm_tab_id
- › Add the UUID to localStorage.storm_open_tabs (the shared list)
- › Register a beforeunload listener: on tab close, remove this UUID from storm_open_tabs; if the list is now empty, clear storm_splash_shown
- › Check localStorage.storm_splash_shown — if set, skip the splash; otherwise show it
- › When user dismisses the splash, write storm_splash_shown = "1" to localStorage
Now opening a new tab reads localStorage.storm_splash_shown (which is shared) and finds it already set. No splash. When the last tab closes, beforeunload fires, the tab list empties, and storm_splash_shown is deleted. The next browser session starts clean.
One important detail: any other component that conditionally starts based on whether the splash was shown must also be updated. If it previously checked sessionStorage.getItem('storm_entered'), change it to localStorage.getItem('storm_splash_shown'). Otherwise it will not start when the splash is skipped on a new tab.
Option B — BroadcastChannel API
BroadcastChannel is a modern browser API that lets open tabs of the same origin send messages to each other in real time. When one tab dismisses the splash, it broadcasts a message and all other open tabs receive it.
This approach works well for real-time sync between already-open tabs. If Tab 1 dismisses the splash, Tab 2 (if already open) hides it instantly.
The limitation: BroadcastChannel only delivers messages to tabs that are already open and listening. It cannot reach a tab that opens after the message was sent. So if you dismiss the splash in Tab 1 and then open Tab 2, Tab 2 starts with an empty sessionStorage, no message arrives (the channel is not live yet), and the splash shows again.
You can combine BroadcastChannel with a sessionStorage check to patch this: on Tab 2 open, query existing tabs via BroadcastChannel for the current state. But this adds latency and complexity — you have to wait for a response before deciding whether to show the splash, which introduces a flash of content or a timing race.
Comparing the Two Approaches
| Concern | Option A (localStorage + tabs) | Option B (BroadcastChannel) |
|---|---|---|
| Works for newly opened tabs | Yes — reads shared localStorage flag | No — message already sent, new tab misses it |
| Real-time sync to existing tabs | No — passive read only | Yes — instant message delivery |
| Resets when all tabs close | Yes — beforeunload clears the flag | Yes — sessionStorage cleared naturally |
| Browser support | Universal | All modern browsers, no IE |
| Implementation complexity | Low to medium | Medium to high (query/response pattern needed) |
| Crash resilience | Partial — beforeunload may not fire on crash | Same limitation |
Why the beforeunload Limitation Is Acceptable
Both approaches have the same crash edge case: if the browser crashes or is force-killed, beforeunload does not fire. The tab IDs stay in localStorage.storm_open_tabs, storm_splash_shown never gets cleared, and the next browser session skips the splash.
For a splash screen this is acceptable. You can add a TTL check if needed — store a timestamp alongside the shown flag and reset it if it is older than 24 hours. But for most portfolios and marketing sites, the crash recovery is unnecessary complexity.
Key Takeaways
- sessionStorage is scoped per browser tab, not per browser session — never use it for cross-tab shared state
- localStorage is shared across all tabs of the same origin and is the right primitive for cross-tab flags
- Track active tabs with a JSON array in localStorage and beforeunload to detect when all tabs close
- Any component that reads the "splash shown" flag must be updated to read from the same localStorage key
- BroadcastChannel is better for real-time tab sync, but cannot retroactively notify newly opened tabs
The Web Storage spec is clear about this — sessionStorage is per-tab by design. It is not a bug to fix in the browser. It is a choice you make about which storage primitive matches your actual requirement. Cross-tab state belongs in localStorage.