Source Code

<div class="demo">
  <div class="profile-card" id="profileCard">
    <div class="skeleton-row">
      <div class="skeleton skeleton-avatar"></div>
      <div class="skeleton-col">
        <div class="skeleton skeleton-line" style="width: 60%"></div>
        <div class="skeleton skeleton-line" style="width: 40%"></div>
      </div>
    </div>

    <div class="posts-section" id="postsSection">
      <div class="posts-placeholder" id="postsPlaceholder">
        <span class="posts-placeholder-text">Waiting for profile before loading posts…</span>
      </div>
    </div>
  </div>
</div>

Cascading Dependency Skeleton Loader — Honest Loading States for Sequential Requests

Cascading Dependency Skeleton Loader — Parent Then Children, Honestly · Loaders · Plain HTML, CSS & JS · Live preview

What's included

Features

Models a genuine sequential data dependency (posts request requires the profile's resolved id) rather than an artificial staged reveal
Distinct "waiting on parent" placeholder for a not-yet-startable request, visually different from an actual in-flight skeleton
The dependent section's skeleton only appears once its request has genuinely begun, keeping the UI honest about request state
Sequential awaits in the loading function structurally enforce the same dependency the UI visually communicates
Shimmering skeleton animation clearly distinguishes "content is being fetched" from both "not started yet" and "loaded"
Fade-in transition softens the visual replacement of skeleton markup with real content in both sections
Clear separation of concerns between skeleton-rendering functions and real-content-rendering functions for each section

About this UI Snippet

Cascading Dependency Skeletons — Being Honest About What's Actually Loading

Screenshot of the Cascading Dependency Skeleton Loader — Parent Then Children, Honestly snippet rendered live

Most skeleton-loading implementations assume every section of a page can start fetching its data independently and simultaneously — but that's not always true. Sometimes a section's request genuinely cannot begin until an earlier request returns something it depends on (a classic example: you can't fetch a user's posts until you know the user's ID, which itself comes from a profile request). This snippet models that real dependency correctly, and — more importantly — makes the loading UI honestly reflect it.

Two visually distinct "not ready yet" states, for two different reasons

Before the profile loads, the posts section shows a plain, muted "waiting for profile before loading posts…" text placeholder — deliberately *not* a shimmering skeleton block. Once the profile resolves and the posts request actually begins, the posts section switches to a real animated skeleton. This distinction matters: a shimmering skeleton visually implies "a request for this content is currently in flight," which would be false during the waiting period — no posts request has been made yet, because it structurally cannot be made until the author id from the profile response exists. Showing the same shimmering skeleton for both states would be a small but real UI lie.

The dependency is enforced by the actual code structure, not just visually implied

load()'s await fetchProfile() is followed by renderPostsSkeleton() and *then* await fetchPosts(profile.id) — the posts request literally cannot be constructed before this point, since it requires profile.id as an argument. This isn't a UI-only distinction layered on top of two independent, already-parallel requests; the sequential dependency is real at the code level, and the loading UI is simply being honest about that existing structure rather than manufacturing an artificial-looking staged reveal.

Why this differs from a page that could fetch everything in parallel

If posts could genuinely be fetched independently of the profile (for example, if the posts endpoint took a URL slug rather than a numeric author id derived from the profile response), the correct pattern would be firing both requests simultaneously with Promise.all and showing two independent skeletons from the start — no "waiting" placeholder needed, since there'd be no real reason for one section to wait on the other. This pattern specifically models the case where a true data dependency exists, and is exactly the wrong pattern to reach for when sections genuinely can load in parallel.

A fade-in on each section once its real content replaces its skeleton

Both renderProfile() and renderPosts() add a section-fade-in class the moment they replace skeleton markup with real content — a small transition that softens the visual "pop" of content suddenly appearing, applied consistently to both the profile and posts sections despite them completing their loading at different, sequential times.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant to explain the difference between a genuine sequential data dependency (where one request's parameters come from another's response) and content sections that merely happen to be rendered one after another, and why the loading UI should differ between the two cases. It's also worth asking for a version that handles a THREE-level cascade (e.g. profile, then posts, then comments on the first post), or one that shows an appropriate error state for the dependent section if the parent request succeeds but the child request subsequently fails.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a cascading (parent-then-child) skeleton loading UI in HTML, CSS, and vanilla JavaScript — no external library.

Requirements:
- A profile section and a posts section on the same card, where fetching the posts genuinely requires an identifier that only becomes available from the resolved profile response (simulate both requests with a delayed Promise, with the posts fetch function accepting the profile's resolved id as a required parameter).
- Before the profile has loaded, the posts section must show a plain, visually distinct placeholder (NOT a shimmering skeleton) indicating it is waiting on the profile to resolve first — since no posts request can have started yet at that point.
- The profile section shows a shimmering skeleton animation while its own request is in flight, then replaces it with real rendered content once resolved.
- The exact moment the profile request resolves, the posts section must switch from its "waiting" placeholder to a genuine shimmering skeleton animation — because only at that point does the posts request actually begin (now that the required id is available) — then replace that skeleton with real rendered post content once its own request resolves.
- Structure the loading logic (the sequence of awaited async calls) so the dependency between the two requests is enforced by the actual code, not merely implied visually — the posts-fetching function must require the profile's resolved id as an argument.
- Apply a brief fade-in transition when skeleton markup in either section is replaced with real content.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Observe the initial stateThe profile section shows a shimmering skeleton; the posts section shows a plain muted "waiting for profile" message — not a shimmering skeleton, since no posts request is in flight yet.
  2. 2
    Watch the profile resolveReal profile content fades in, replacing its skeleton.
  3. 3
    Watch the posts section change stateThe moment the profile resolves, the posts section switches from its "waiting" placeholder to a REAL shimmering skeleton — because only now has the posts request actually started.
  4. 4
    Watch the posts resolveReal post content fades in, replacing the posts skeleton, completing the cascade.
  5. 5
    Adapt fetchProfile()/fetchPosts() to real requestsReplace both with real API calls, keeping fetchPosts() requiring the resolved profile's id as its argument to preserve the genuine dependency structure.

Real-world uses

Common Use Cases

PROFILE
Profile pages with dependent content sections
A user profile where posts, activity, or stats genuinely require the profile's resolved id before they can be requested.
DASHBOARD
Dashboards with hierarchical data dependencies
A dashboard where a selected account or workspace must resolve before its child resources (projects, members) can be fetched.
DETAIL
Master-detail views
Any UI where selecting or loading a parent record is a genuine prerequisite for fetching its related child records.
APIs with resource-dependent endpoints
Backend structures where a child resource's endpoint requires an id only available from a parent resource's response.
Related: 3D Rotating Cube Loader
See the 3D Rotating Cube Loader for a related loaders pattern worth pairing with this one.
Related: Skeleton Shape Morph Reveal
See the Skeleton Shape Morph Reveal for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

A shimmering skeleton visually communicates "a request for this content is currently in flight" — which would be false during that waiting period, since the posts request cannot even be constructed yet without the profile's resolved id. The plain, muted placeholder honestly communicates a different state: waiting on a prerequisite, not actively loading.

It's a real, enforced dependency — fetchPosts() requires profile.id as an argument, and that argument literally does not exist until fetchProfile()'s promise has resolved. The sequential await structure in load() is not just a visual staging trick; the posts request genuinely cannot be made any earlier.

When two pieces of content can genuinely be fetched independently and in parallel (neither requires data from the other's response), you should fire both requests simultaneously (e.g. with Promise.all) and show two independent skeletons from the start — introducing an artificial "waiting" state for content that could have loaded in parallel would only slow down the perceived loading time for no real reason.

It immediately switches from its "waiting for profile" placeholder to a real shimmering skeleton, because the posts request genuinely begins at that exact moment (with profile.id now available) — the skeleton's appearance is tied to the request actually starting, not to an arbitrary timer.

Replace fetchProfile() and fetchPosts() with real requests to your backend, keeping fetchPosts() accepting the resolved profile's id (or whichever identifier your posts endpoint genuinely requires) as its parameter, preserving the real sequential dependency between the two calls.

It softens the visual "pop" of content suddenly appearing in place of a skeleton, applied consistently to both sections even though they complete their loading at different points in the sequence — a small polish detail that makes each transition feel more intentional than an abrupt content swap.