✦ A decade of Canvas craft, now driven by AI — describe it, watch it build live.Start building
Bootstrap 5Feedback

Bootstrap 5 Skeleton Loader

A skeleton loader is a placeholder UI that mimics the shape and layout of real content whilst data is loading, reducing perceived wait time and preventing layout shift. It is composed entirely from Bootstrap 5 utilities — primarily bg-secondary (or bg-body-secondary in 5.3), rounded, d-flex, gap, and w-* classes — combined with a CSS pulse animation defined in an inline style block. Use it whenever content loads asynchronously: API calls, lazy-loaded images, or paginated feeds.

Primary Class

bg-body-secondary rounded placeholder-wave

Common Use Cases

  • A social media dashboard shows skeleton cards for each post whilst the activity feed loads from a REST API, preventing the page from appearing broken.
  • An e-commerce product listing page renders skeleton grid items in place of product images and prices during the initial data fetch, keeping the layout stable.
  • A user profile page displays a circular skeleton avatar and three lines of skeleton text whilst the user record is retrieved from an authentication service.
  • A data table renders skeleton rows with varying column widths whilst a paginated report dataset is fetched, giving users an accurate preview of the incoming table structure.

Variants & Classes

VariantDescription
Text Block SkeletonStacked horizontal bars of varying widths simulate a paragraph of text. Uses Bootstrap w-* utilities (w-100, w-75, w-50) on thin div elements styled with a fixed height.
Card SkeletonCombines a full-width image placeholder with a text block skeleton inside a Bootstrap card shell, composing .card, .card-body, and flex utilities.
Avatar and Text Row SkeletonPlaces a circular placeholder beside stacked text bars using d-flex, align-items-center, and gap-3 — common in comment lists, user tables, and notification panels.
Table Row SkeletonRenders skeleton cells inside a standard Bootstrap table structure, using col-span-aware widths and the pulse animation on each td's inner div to match real table content.

Code Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Bootstrap 5 Skeleton Loader</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
  <style>
    .skeleton-pulse {
      animation: skeleton-pulse-anim 1.5s ease-in-out infinite;
    }
    @keyframes skeleton-pulse-anim {
      0%   { opacity: 1; }
      50%  { opacity: 0.4; }
      100% { opacity: 1; }
    }
    .skeleton-line {
      height: 0.85rem;
      border-radius: 0.25rem;
    }
    .skeleton-img {
      height: 180px;
      border-radius: 0.375rem 0.375rem 0 0;
    }
    .skeleton-avatar {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      flex-shrink: 0;
    }
  </style>
</head>
<body class="bg-light p-4">
  <div class="container">
    <h1 class="h4 mb-4">Skeleton Loaders</h1>

    <!-- Card skeleton -->
    <div class="row g-4 mb-5">
      <div class="col-12 col-sm-6 col-md-4" aria-busy="true" aria-label="Loading content">
        <div class="card border-0 shadow-sm overflow-hidden">
          <div class="skeleton-img skeleton-pulse bg-body-secondary w-100"></div>
          <div class="card-body">
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-75 mb-2"></div>
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-100 mb-2"></div>
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div>
          </div>
        </div>
      </div>
      <div class="col-12 col-sm-6 col-md-4" aria-busy="true" aria-label="Loading content">
        <div class="card border-0 shadow-sm overflow-hidden">
          <div class="skeleton-img skeleton-pulse bg-body-secondary w-100"></div>
          <div class="card-body">
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-75 mb-2"></div>
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-100 mb-2"></div>
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div>
          </div>
        </div>
      </div>
      <div class="col-12 col-sm-6 col-md-4" aria-busy="true" aria-label="Loading content">
        <div class="card border-0 shadow-sm overflow-hidden">
          <div class="skeleton-img skeleton-pulse bg-body-secondary w-100"></div>
          <div class="card-body">
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-75 mb-2"></div>
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-100 mb-2"></div>
            <div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div>
          </div>
        </div>
      </div>
    </div>

    <!-- Avatar + text row skeleton -->
    <h2 class="h6 text-muted mb-3">User List Skeleton</h2>
    <div class="d-flex flex-column gap-3 mb-5">
      <div class="d-flex align-items-center gap-3" aria-busy="true" aria-label="Loading user">
        <div class="skeleton-avatar skeleton-pulse bg-body-secondary"></div>
        <div class="flex-grow-1">
          <div class="skeleton-line skeleton-pulse bg-body-secondary w-50 mb-2"></div>
          <div class="skeleton-line skeleton-pulse bg-body-secondary w-75"></div>
        </div>
      </div>
      <div class="d-flex align-items-center gap-3" aria-busy="true" aria-label="Loading user">
        <div class="skeleton-avatar skeleton-pulse bg-body-secondary"></div>
        <div class="flex-grow-1">
          <div class="skeleton-line skeleton-pulse bg-body-secondary w-50 mb-2"></div>
          <div class="skeleton-line skeleton-pulse bg-body-secondary w-75"></div>
        </div>
      </div>
      <div class="d-flex align-items-center gap-3" aria-busy="true" aria-label="Loading user">
        <div class="skeleton-avatar skeleton-pulse bg-body-secondary"></div>
        <div class="flex-grow-1">
          <div class="skeleton-line skeleton-pulse bg-body-secondary w-50 mb-2"></div>
          <div class="skeleton-line skeleton-pulse bg-body-secondary w-75"></div>
        </div>
      </div>
    </div>

    <!-- Table skeleton -->
    <h2 class="h6 text-muted mb-3">Table Skeleton</h2>
    <table class="table table-borderless" aria-busy="true" aria-label="Loading table data">
      <thead>
        <tr>
          <th><div class="skeleton-line skeleton-pulse bg-body-secondary w-75"></div></th>
          <th><div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div></th>
          <th><div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div></th>
          <th><div class="skeleton-line skeleton-pulse bg-body-secondary w-25"></div></th>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-100"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-75"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-75"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div></td>
        </tr>
        <tr>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-100"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-75"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div></td>
        </tr>
        <tr>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-100"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-75"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-50"></div></td>
          <td><div class="skeleton-line skeleton-pulse bg-body-secondary w-25"></div></td>
        </tr>
      </tbody>
    </table>
  </div>
</body>
</html>

Live Examples

Text Paragraph Skeleton

A minimal text-only skeleton with four lines of varying width, suitable for article bodies or description panels loading asynchronously.

Example 1

Avatar and Name Row Skeleton

Circular avatar placeholder alongside two text lines — ideal for comment threads, notification lists, or team member directories awaiting API data.

Example 2

Product Card Skeleton

Full card skeleton with an image block and three text lines inside a Bootstrap card, matching the layout of a typical e-commerce product tile.

Example 3

Canvas Framework Variants

The Canvas template extends Bootstrap 5 with 1,658+ component variants. Generate any of these using Canvas Builder:

  • Canvas Builder: generate a 3-column skeleton card grid from a single prompt
  • Canvas Builder: avatar-and-text skeleton list for a notifications panel
  • Canvas Builder: full-page skeleton layout with sidebar and main content area
  • Canvas Builder: skeleton table with header and five data rows
  • Canvas Builder: dashboard skeleton with stat tiles and a chart placeholder

Best Practices

Use aria-busy and aria-label on the container

Always add aria-busy="true" to the outermost skeleton wrapper so screen readers announce that content is loading. Pair it with a descriptive aria-label such as "Loading product list" so assistive technology users understand what to expect. Remove the skeleton from the DOM entirely once real content is available — do not just hide it with d-none, as hidden aria-busy elements can still confuse some screen readers.

Match skeleton dimensions to real content

The skeleton loses its value if its proportions differ significantly from the real content it replaces, because users will see a jarring layout shift on load. Measure the actual rendered heights and widths of your headings, images, and text lines, then hard-code those values in inline styles or a small custom CSS block. Bootstrap's w-* utilities cover 25, 50, 75, and 100 per cent widths, which is usually sufficient for text lines but image blocks typically need an explicit height.

Prefer opacity animation over background-color shimmer for broad browser support

A shimmer effect (moving gradient left to right) requires a CSS background-size and animation-direction: alternate approach that can conflict with Bootstrap's bg-* utilities when dark mode is active. A simple opacity pulse defined as a single @keyframes block avoids this entirely, works correctly in both Bootstrap 5.3 light and dark colour modes (bg-body-secondary adapts automatically), and has no dependency on JavaScript. If you do want a shimmer, override the background with a standalone CSS custom property rather than touching Bootstrap's colour utilities.

FAQ

Does Bootstrap 5 ship a skeleton loader component?
No. Bootstrap 5 has a .placeholder and .placeholder-wave utility introduced in v5.1 that can create basic inline text placeholders, but it does not provide a full skeleton loader pattern with image blocks, avatar circles, or card layouts. The markup on this page composes Bootstrap's bg-body-secondary, rounded, d-flex, gap-*, w-*, and card utilities with a small custom @keyframes pulse animation to build the complete pattern.
Can I use Bootstrap's built-in .placeholder class instead of custom CSS?
Bootstrap's .placeholder class applies opacity reduction to inline elements such as spans inside headings or paragraph tags, which works well for short text placeholders. However, it is not designed for block-level shape placeholders like image areas, avatars, or card bodies. For those shapes you need explicit height values and border-radius, which Bootstrap's .placeholder does not set, so the custom approach shown here gives you full control without fighting the framework.
How do I switch from the skeleton to real content once data has loaded?
The recommended approach is to render the skeleton and the real content as sibling elements in your template, then toggle a loading state class or attribute on the parent. When data resolves, remove the skeleton element from the DOM (or conditionally render it in your framework) and make the real content visible. Avoid toggling visibility or opacity alone, as the skeleton element will still occupy space and can cause flicker. In vanilla JavaScript, a simple parent.querySelector('.skeleton-wrapper').remove() after your fetch resolves is sufficient.
How do I support Bootstrap 5.3 dark mode with these skeletons?
Use bg-body-secondary rather than bg-secondary or a hard-coded grey hex value. The bg-body-secondary utility is mapped to a CSS custom property that Bootstrap 5.3 automatically swaps when data-bs-theme="dark" is set on the html element or any ancestor. This means your skeleton colours adapt to dark mode with zero extra CSS. If you are on Bootstrap 5.2 or earlier, bg-body-secondary does not exist; use bg-secondary bg-opacity-25 as a fallback.