Source Code

<div class="container py-5 d-flex justify-content-center">
  <div class="card bsskel-card">
    <div class="card-body p-3">
      <div class="d-flex justify-content-between align-items-center mb-2">
        <h6 class="fw-bold mb-0">Team members</h6>
        <button type="button" class="btn btn-sm btn-outline-secondary" id="bsskelReload">Reload data</button>
      </div>
      <table class="table table-sm align-middle mb-0">
        <thead>
          <tr><th>Name</th><th>Email</th><th>Role</th><th>Status</th></tr>
        </thead>
        <tbody id="bsskelBody"></tbody>
      </table>
    </div>
  </div>
</div>

Bootstrap Data Table Loading Skeleton — Free HTML CSS JS Snippet

Bootstrap Data Table Loading Skeleton · Tables · Plain HTML, CSS & JS · Live preview

What's included

Features

Skeleton bar widths are shaped per column instead of being uniform placeholder rectangles
The shimmer animation runs entirely on background-position, with zero per-frame JavaScript
renderSkeleton() and renderData() both target the same tbody, so no other markup changes between states
The Reload button disables itself for the duration of the load, preventing overlapping fetch simulations
The row count and column count of the skeleton matches the real data, not an arbitrary placeholder shape

About this UI Snippet

Bootstrap Data Table Loading Skeleton — HTML, CSS & JavaScript

Screenshot of the Bootstrap Data Table Loading Skeleton snippet rendered live

A skeleton only works if it roughly matches the shape of what's coming — five identical gray rectangles read as generic "loading," while bars sized to each column's typical content read as "this table, loading." That's what the WIDTHS array does here: each of the four <td> bars gets a different percentage width (70%, 85%, 60%, 50%) echoing that a name is usually shorter than an email, which is usually longer than a role label.

The shimmer itself is a single CSS background-position animation on a gradient that's four times wider than the element (background-size: 400% 100%) — animating its position sweeps a lighter band across each bar continuously, entirely on the compositor thread, with no JavaScript driving the visual effect frame by frame. JavaScript's only job is swapping which markup is in #bsskelBody at all: renderSkeleton() for the loading state, renderData() for the resolved one, both writing into the exact same tbody so nothing about the surrounding table needs to change between states.

The Reload button disables itself for the duration of the simulated fetch specifically so a second click can't restart the skeleton mid-load and leave two competing timers racing to populate the same tbody — the same overlapping-request problem bootstrap-form-autosave-status solves for a save action instead of a fetch.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Hand this snippet to an AI coding assistant like Claude and ask it to make the skeleton row count match a real API's expected page size automatically, or to add a minimum-display-time guard so the skeleton never flashes for an imperceptibly short moment on a very fast connection.

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 Bootstrap 5.3 data table with a loading skeleton state, using the real Bootstrap CDN framework (bootstrap.min.css and bootstrap.bundle.min.js), not custom CSS made to resemble it.

Requirements:
- A table with 4 columns and a "Reload data" button above it.
- On load, and again on every Reload click, first render 5 skeleton rows whose cells are shimmering gray bars — give each column's bar a different width so the skeleton's shape roughly matches the real content's shape (e.g. a shorter bar for a short column, a longer one for a long column).
- The shimmer effect must be a pure CSS animation (an oversized background gradient animated via background-position), not a JavaScript-driven frame-by-frame effect.
- After a simulated delay (around 1.4 seconds via setTimeout), replace the skeleton rows with 5 rows of real sample data in the same table.
- Disable the Reload button for the duration of the simulated load so a second click cannot start an overlapping load.

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
    Load the snippetFive shimmering skeleton rows appear immediately, each bar sized differently per column.
  2. 2
    Wait about 1.4 secondsThe skeleton rows are replaced by five real rows of names, emails, roles, and status badges.
  3. 3
    Click "Reload data"The skeleton reappears and the button disables itself until the simulated fetch resolves again.
  4. 4
    Click Reload again quicklyNothing happens while it's disabled — a second load can't start until the first one finishes.

Real-world uses

Common Use Cases

Dashboards and admin tables backed by an API
Replace the setTimeout with a real fetch call — pairs naturally with bootstrap-data-table-error-state for the failure path a real request also needs.
Any table that loads after an initial page render
Avoids a layout jump between "nothing" and "a full table" by showing the table's eventual shape immediately.
CART
Order history and account activity panels
Pairs with bootstrap-sticky-table-header-scroll for a longer real-data table that also loads asynchronously.
Learning shape-matched loading states
A direct, working comparison point against a generic spinner overlay — see how differently each communicates what's about to appear.

Got questions?

Frequently Asked Questions

A spinner (see bootstrap-loading-spinner-overlay) communicates "something is happening" with no hint of what. A skeleton shaped like the real table communicates the eventual layout immediately, which most research on perceived performance shows feels faster even at the identical actual load time.

Uniform bars read as a generic loading placeholder; bars sized like real content (a name column narrower than an email column) make the skeleton specifically resemble this table rather than any table, which is what makes the transition to real data feel seamless instead of jarring.

No — it is a pure CSS @keyframes animation on background-position over an oversized gradient. JavaScript is only responsible for swapping the skeleton markup for real data once the (simulated) load finishes.

Yes. Track a loading boolean in component state and conditionally render either a mapped array of skeleton rows or the real data rows from the same array-mapping logic — the CSS shimmer class needs no changes.

Replace the setTimeout in load() with an actual fetch, calling renderData() with the resolved response inside .then() and keeping the skeleton visible (and Reload disabled) for the full duration of the real request.