webdevpuneet.com

Skeleton Table — Free HTML CSS JS Loader Snippet

Skeleton Table · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="skt">
  <div class="skt-head">
    <h3>Team members</h3>
    <button class="skt-reload" id="sktReload" type="button">
      <svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
      Reload
    </button>
  </div>
  <table class="skt-table" aria-busy="true" id="sktTable">
    <thead>
      <tr><th>Name</th><th>Role</th><th>Status</th><th class="skt-r">Last active</th></tr>
    </thead>
    <tbody id="sktBody"></tbody>
  </table>
</div>

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

Single bone primitive
One .skt-bone class with a sliding oversized gradient serves circles, bars, and pills via per-use size overrides.
Synchronised shimmer
All bones share one keyframe and duration so the highlight sweeps the entire table in unison.
Structure-mirrored rows
Skeleton and real rows emit identical tr/td structure, guaranteeing zero layout shift on swap.
Varied bone widths
Widths derive from the row index so consecutive skeleton rows differ — no stamped-copy look.
aria-busy contract
The table announces its loading state to assistive tech and flips to false when real rows land.
Reduced-motion fallback
prefers-reduced-motion freezes the shimmer to flat grey for vestibular safety.
Staggered row entrance
Real rows rise in with 60ms cascading delays instead of a hard content cut.
Debounced reload
load() clears any pending timer first so repeated reloads never queue overlapping swaps.

About this UI Snippet

Skeleton Table — Shimmer Loading Placeholder That Swaps Into Real Table Rows

Screenshot of the Skeleton Table snippet rendered live

Skeleton screens beat spinners for data tables because they preserve layout: the user sees the table's shape immediately, and when data arrives nothing jumps. The catch is that a table skeleton only works if the placeholder rows genuinely match the real rows — same columns, same cell heights, same visual weight. This component builds that properly in HTML, CSS, and vanilla JavaScript: shimmering bone placeholders shaped like the final avatar, text, and status-pill content, an aria-busy accessibility contract, and a staggered fade-in when real rows replace the bones.

One bone primitive, many shapes

Everything skeletal is a single .skt-bone class — an inline-block with a three-stop grey linear-gradient whose background-size is 400% width, animated by sliding background-position from one end to the other. Because the gradient is wider than the element, a soft highlight band appears to sweep across each bone. Width, height, and border-radius are then overridden per use: a 32px circle for the avatar, a 999px-radius lozenge for the status pill, and various text bars. One keyframe animation serves every bone, and since all bones share the same duration and timing, the shimmer sweeps the whole table in unison — the coordinated look users recognise from Facebook and LinkedIn.

Skeleton rows that mirror real rows

skeletonRow() emits the exact same <tr>/<td> structure as realRow() — the same .skt-user flex layout in the name cell, the same right-aligned last column — with bones sized like the content they stand in for. Two widths are derived from the row index (90 + (i % 3) * 18) so consecutive rows differ slightly; identical stamped rows are the tell that makes skeletons look fake. Because both renderers agree on structure, the swap to real data cannot shift the layout: columns were already at their final widths.

The loading contract: aria-busy

While bones are showing, the table carries aria-busy="true", which tells assistive technology the region is updating and its current content is not meaningful — screen readers won't announce a page full of decorative divs. When the data lands, the attribute flips to false and the real rows are announced normally. The demo also honours prefers-reduced-motion by freezing the shimmer to a flat grey, since large animated regions are a vestibular trigger.

Staggered swap-in

Real rows arrive with a .skt-in class animating a 5px rise-and-fade, each delayed 60ms after the previous via an inline animation-delay. The cascade reads as "data flowing in" rather than a hard cut, the same entrance choreography as the stagger list. A reload button (whose icon spins while loading) restarts the cycle, and the load() function clears any in-flight timer first so rapid clicks cannot queue overlapping swaps.

Where your fetch goes

The 1.8-second setTimeout stands in for a network request. In production, load() becomes: render skeletons, await fetch(), render real rows in the response handler, flip aria-busy. Because skeleton and real rendering are separate pure functions over the same structure, wiring a real API changes one line. If your row count is unknown ahead of time, render 5–8 skeleton rows — enough to fill the visible area without implying a precise count.

Customisation

Adjust the bone gradient greys for dark mode (dark base, slightly lighter sweep), tune the 1.4s shimmer, and extend the row templates to your columns — every new cell just needs a matching bone. For whole-page loading states, combine with the skeleton dashboard and skeleton card grid variants.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSA team-members table renders in its loading state — five skeleton rows of shimmering avatar circles, text bars, and pill lozenges.
  2. 2
    Watch the swapAfter the simulated 1.8s fetch, real rows cascade in with a 60ms stagger — avatars, roles, status pills, and timestamps land without any layout shift.
  3. 3
    Click ReloadThe button's icon spins, skeletons return instantly, and the cycle repeats; rapid clicks are debounced by clearing the pending timer.
  4. 4
    Check reduced motionWith prefers-reduced-motion enabled, bones render as static grey blocks instead of shimmering.
  5. 5
    Wire your APIReplace the setTimeout in load() with your fetch call — render skeletons before the request and realRow() output in the response handler.
  6. 6
    Match your columnsExtend skeletonRow() and realRow() together — every real cell gets a bone sized like its content so the swap stays shift-free.

Real-world uses

Common Use Cases

Admin dashboards
The loading state for any user or record table — pair with a data table or sortable table once loaded.
SaaS member lists
Team pages that fetch on mount get instant perceived structure; the status pills match a team presence list.
Search results tables
Re-show skeletons on every query change so result swaps feel deliberate, not flickery.
Infinite and paginated tables
Append skeleton rows below existing data while the next page loads — see infinite scroll table.
Mobile app webviews
Skeletons mask slow mobile networks far better than a centred spinner.
Learning skeleton technique
A reference for the oversized-gradient shimmer, structure mirroring, and the aria-busy loading contract.
Related: Optimistic Action Button with Rollback on Failure
See the Optimistic Action Button with Rollback on Failure for a related loaders pattern worth pairing with this one.
Related: Skeleton to Content Crossfade
See the Skeleton to Content Crossfade for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Each bone's background is a linear-gradient with a lighter middle stop, sized to 400% of the element's width via background-size. The keyframe slides background-position from 100% to 0, moving the wide gradient across the narrow element so the light band sweeps through. Because it animates background-position on small elements rather than layout properties, it stays cheap even with dozens of bones on screen.

The entire point of a skeleton is zero layout shift: the browser computes final column widths and row heights from the skeleton markup, so when real content replaces it nothing reflows. If the skeleton were a generic block overlay, the table would snap to different dimensions when data arrived — visually worse than a spinner. Mirroring structure means every td, flex wrapper, and cell alignment is identical between skeletonRow() and realRow().

aria-busy="true" tells screen readers the element is being updated and its contents should not be treated as final — without it, assistive tech may announce meaningless placeholder markup. Flipping it to "false" after the swap signals the content is ready. Pairing this with the prefers-reduced-motion fallback (static grey bones) covers both the announcement and vestibular sides of loading-state accessibility.

Render enough to fill the visible scroll area — typically 5 to 8 for a card-height table. Too few makes the table look nearly empty; matching some exact expected count implies precision you don't have. This snippet renders one skeleton per known row because the demo data is fixed, but ROWS.map can trivially become Array.from({ length: 6 }) when the response size is unknown.

Model a loading boolean (or a data === null state) and render either the skeleton rows or the real rows from it — the two template branches replace innerHTML strings. Set aria-busy from the same state. The fetch lives in useEffect / onMounted / ngOnInit, setting data on resolve. The shimmer gradient, reduced-motion query, and stagger animation are pure CSS and port unchanged; skeleton components in libraries like MUI or shadcn/ui use this identical technique.