webdevpuneet.com

Suspense-Style Fallback Card — Vanilla JS Loading Boundary Pattern

Suspense-Style Data Fetch Fallback · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="sf-card" id="sfCard">
  <div class="sf-fallback" id="sfFallback">
    <div class="sf-avatar"></div>
    <div class="sf-lines"><div class="sf-line sf-l1"></div><div class="sf-line sf-l2"></div><div class="sf-line sf-l3"></div></div>
  </div>
  <div class="sf-content" id="sfContent" hidden>
    <img class="sf-real-avatar" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='48' height='48'%3E%3Ccircle cx='24' cy='24' r='24' fill='%236366f1'/%3E%3Ctext x='24' y='30' font-size='18' text-anchor='middle' fill='white' font-family='sans-serif'%3EJP%3C/text%3E%3C/svg%3E" alt="">
    <div>
      <div class="sf-name">Jordan Park</div>
      <div class="sf-bio">Product designer — resolved after a simulated fetch, exactly like a component read from a Suspense-wrapped data source would.</div>
    </div>
  </div>
</div>
<button type="button" class="sf-retry" id="sfRetry">Refetch</button>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real Suspense mental model
Fallback and resolved content are two pre-built states, swapped on resolve.
Promise-based function
fetchProfile() returns a real Promise, not a bare timeout.
Explicit boundary logic
One function owns exactly when the fallback shows and hides.
Framework-agnostic pattern
The same idea a React Suspense component uses, without React.
Retry built in
A refetch button re-triggers the fallback-then-resolve cycle.
Smooth content entrance
The resolved content fades and lifts in rather than snapping into view.
Shimmer fallback
An avatar-plus-lines skeleton in the meantime.
Zero dependencies
Pure HTML/CSS/JS — no React, no Suspense runtime.

About this UI Snippet

Suspense-Style Fallback Card — The Vanilla-JS Version of a Suspense Boundary

Screenshot of the Suspense-Style Data Fetch Fallback snippet rendered live

React's <Suspense fallback={<Skeleton />}> pattern shows a fallback UI while a component's data is unresolved, then swaps to the real component the instant it resolves — no manual loading-state plumbing inside the component itself. This snippet demonstrates that exact visual pattern without React: a fallback skeleton and the real content both exist in the DOM from the start, and a single fetchProfile() function toggles which one is visible based on whether the simulated "resource" has resolved — the same boundary-and-swap mechanic, written by hand.

Two pre-built states, one visibility toggle

Both the .sf-fallback skeleton and the .sf-content real markup exist in the HTML from page load; only their hidden attribute changes. fetchProfile() shows the fallback and hides the content, waits on a promise standing in for a real request, and on resolve flips hidden the other way. This mirrors what Suspense does structurally — the fallback and the resolved tree are two things React can swap between, not a single element mutating its own inner state — which is why this pattern generalizes so cleanly to any framework or no framework at all.

Why this is useful without React

If you like the Suspense mental model — declare a fallback, declare the real content, let a resolve event handle the swap — but you're not using React (or you're building a plain HTML component), this is the same idea implemented directly: a promise-returning function that owns exactly one job, showing the right state at the right time. It reads as a clean vanilla-JS pattern for "loading boundary" logic that a framework component or hook could wrap later.

A real promise, not a fake delay

fetchProfile returns an actual Promise, resolved here by a setTimeout standing in for fetch(). Because it's a real promise, callers can await it, chain .then(), or race it against a timeout — the same ergonomics as the data-fetching function a Suspense-compatible resource would wrap, just without the resource-cache machinery React needs internally.

Retry included

A "Refetch" button re-invokes fetchProfile(), re-showing the fallback and disabling itself until the new promise resolves — demonstrating that the boundary isn't a one-shot animation, it's a reusable toggle driven by any promise you hand it. Pair the fallback shape with a skeleton loader for other layouts, or an ai thinking loader for a chat-specific fallback.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA skeleton fallback shows immediately, then resolves into a real profile card.
  2. 2
    Watch the swapAfter ~1.6s the fallback hides and the real content fades in.
  3. 3
    Click RefetchThe fallback reappears and the cycle repeats, mirroring a re-suspended boundary.
  4. 4
    Replace the fake promiseSwap the setTimeout in fetchProfile for your real fetch()/API call.
  5. 5
    Swap the fallback shapeChange .sf-fallback's markup to match whatever content it's standing in for.
  6. 6
    Reuse the patternCopy the two-elements-plus-toggle structure for any other async section.

Real-world uses

Common Use Cases

Suspense pattern without React
Bring the fallback-then-resolve idea to a plain JavaScript project, with a boundary function owning exactly when the skeleton shows and hides.
Server-rendered pages with hydration
Show a fallback for a client-fetched section of an otherwise static page, then swap to the resolved content when data arrives.
Self-contained async cards
Make any widget with its own data fetch independent, using a real Promise from fetchProfile() rather than a bare timeout.
Design system references
Provide a framework-agnostic implementation of loading boundaries that React, Vue and Svelte teams can all read.
Teaching and migration
Explain the Suspense concept without a framework, or preserve the same fallback behaviour while moving a project off React.
Related: Particle Swarm Loader
See the Particle Swarm Loader for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

React's <Suspense fallback={<Skeleton/>}> shows the fallback element while a wrapped component's data dependency is unresolved, then swaps to the real component tree once it resolves — without the component itself managing a loading flag. This snippet reproduces that structurally: both the fallback and real content exist as separate elements from the start, and one function toggles which is visible based on a promise's resolution, rather than one element mutating an internal loading state.

No — that's the point. fetchProfile() is a plain function returning a plain Promise, and the "swap" is just toggling the hidden attribute on two existing DOM elements. It works in any HTML page, any framework, or no framework, which makes it useful if you like the Suspense mental model but aren't using React.

Replace the setTimeout-wrapped Promise inside fetchProfile with your real fetch() or async function, keeping the same shape: show the fallback and hide the content at the start, await/resolve the real request, then hide the fallback and show the content in a .then() or after an await. Add a .catch() to show an error state if you need one — this demo omits it for clarity.

Pre-building both states and toggling visibility (rather than injecting the real content's markup only after it resolves) keeps the swap instant and avoids a layout jump from building new DOM nodes at resolve time. It also mirrors how Suspense conceptually treats the fallback and the resolved tree as two ready-made branches, not a dynamically constructed one.

In React, this is literally what <Suspense> plus a Suspense-compatible data source gives you for free. In Vue or Angular (no built-in Suspense-equivalent for arbitrary promises), wrap the same idea in a small component: hold a resolved boolean in state/signal, render the fallback markup when false and the real content when true, and flip it in a promise .then()/await, exactly like fetchProfile() does here.