Skeleton Table — Free HTML CSS JS Loader Snippet

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

Share & Support

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to trace the bone-to-row mapping by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why skeletonRow() and realRow() must emit identical tr/td structure for the zero-layout-shift guarantee to hold, or how the width formula in skeletonRow (90 + (i % 3) * 18) avoids the stamped-copy look. The same assistant can help optimize it, for instance checking whether the debounced load() function correctly prevents overlapping timers on rapid reload clicks, or whether five skeleton rows is the right number when the real result count is unknown. It is just as useful for extending the table: ask it to add a skeleton error state for failed fetches, support column sorting on the loaded table, or generalize skeletonRow so adding a new real column automatically produces a matching bone. Treat the code less like a finished artifact and more like a starting point for a conversation.

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 "skeleton table" loading state in plain HTML, CSS, and JavaScript for a data table with an avatar, name/email, role, status pill, and last-active columns — no libraries.

Requirements:
- A single reusable bone primitive class: an inline-block element with a three-stop grey linear-gradient sized to 400% of the element's own width, animated by sliding background-position across it in one keyframe. Every skeleton shape (circle avatar, text bar, pill lozenge) must be this same class with only width, height, and border-radius overridden per use.
- A skeletonRow(index) function and a realRow(rowData, index) function that emit exactly the same tr/td structure (same wrapper divs, same cell order, same alignment classes) so that when skeleton rows are replaced by real rows, no column width or row height changes — zero layout shift.
- Skeleton bone widths must vary slightly based on the row index (not be identical across all rows) so the placeholder doesn't look like a stamped copy of a single row.
- The table element must carry aria-busy="true" while skeleton rows are showing and aria-busy="false" once real rows have rendered, so assistive technology does not announce placeholder content as meaningful.
- Real rows, when they replace the skeletons, must animate in with a fade-and-rise (opacity plus a small translateY) staggered by an increasing delay per row index, so data appears to cascade in rather than snapping in all at once.
- A reload control must restart the full skeleton-then-data cycle, and must clear any previously pending simulated-fetch timer first so rapid repeated clicks cannot queue overlapping swaps.
- Respect prefers-reduced-motion by freezing the bone shimmer to a static flat color instead of animating it.

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
    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.

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.