Skip to content
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. Bootstrap's own name for the pattern is placeholders (.placeholder, .placeholder-glow and .placeholder-wave), available since 5.1.

Bootstrap class

.placeholder, .placeholder-glow, .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 and classes

VariantDescription and classes
Native placeholdersBootstrap's built-in loading shapes, sized with col-* and placeholder-lg, -sm or -xs.placeholder col-6 + placeholder-glow / placeholder-wave
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.bg-body-secondary rounded mb-2
Card SkeletonCombines a full-width image placeholder with a text block skeleton inside a Bootstrap card shell, composing .card, .card-body, and flex utilities.card border-0 shadow-sm
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.d-flex align-items-center gap-3
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.table table-borderless

Code example

HTML
<!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

Native Bootstrap placeholders

Bootstrap's built-in .placeholder classes: widths from col-* classes and a pulse from placeholder-glow, no custom CSS.

Example 1

Text Paragraph Skeleton

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

Example 2

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 3

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 4

Canvas template variants

Canvas Builder builds pages on the Canvas HTML template, which adds its own styled versions of Bootstrap 5 components. Variants you can ask for in a prompt:

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

Disable interactive elements inside skeletons with the disabled class

Any buttons or links inside a skeleton block should carry both the disabled and placeholder classes. This prevents accidental clicks and visually signals the non-interactive state without extra JavaScript.

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?
Yes, for text lines and buttons. .placeholder is an inline-block with a minimum height of 1em; size it with col-* width classes and placeholder-lg, -sm or -xs, and animate it with placeholder-glow or placeholder-wave on a parent (see the first example). For image areas, avatars or card media, give the shape an explicit width and height, or use a block with bg-body-secondary as in the other examples, because .placeholder does not set a height for block shapes.
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.
What is the difference between placeholder-glow and placeholder-wave?
Both are animation modifier classes applied to a parent element. placeholder-glow animates the opacity of child placeholder spans, creating a fading pulse effect. placeholder-wave slides a highlight across the spans from left to right, similar to the shimmer effect popularised by Facebook's skeleton screens. Choose placeholder-wave for a more distinct loading indication; choose placeholder-glow for something subtler that blends into minimal UI designs.
How do I control the height and colour of individual placeholder spans?
By default, placeholder spans inherit their height from font-size, so placeholder-lg and placeholder-sm utility classes adjust size. For custom heights, apply an inline style or a utility class like style='height: 1.5rem;'. Colour is controlled via Bootstrap background colour utilities: e.g., adding bg-secondary or bg-dark to the span itself overrides the default muted grey, letting you match your brand's loading aesthetic.