NEWVersion 1.0 is here.
OpenTailwindv1.0
BlocksTemplatesCollectionsGuidesPricing
Tailwind CSS Skeleton Loading: A Working Example
BlogTailwind CSS Skeleton Loading: A Working Example
TutorialOctober 14, 2026·5 min read

Tailwind CSS Skeleton Loading: A Working Example

Build a skeleton loading state with a working demo, stable layout, reduced motion, accessible status updates, and error-state guidance.

By OpenTailwind, by lindo.ai

On this page

  1. A working card example
  2. Choose the right loading treatment
  3. Make loading understandable without the animation
  4. Adapt the shape for tables and dashboards
  5. Handle errors, empty results, and reduced motion
  6. Before shipping
  7. Sources and further reading

A skeleton loader is a temporary outline of content that has not arrived yet. Use it when you know the shape of the incoming interface, such as a product card or table row. Keep the placeholder close to the final dimensions, announce the loading state once, and replace it with content or an actionable error.

Skeletons do not make a request faster. They communicate what is coming and can help reserve space. For a short button action, a busy label is usually clearer; for an unknown operation, a spinner with descriptive text may communicate more honestly than a fake page outline.

A working card example

Example setup: Tailwind CSS 4.1.18, compiled locally, with plain HTML and vanilla JavaScript. These small educational examples are separate from the membership library. Adapt them to your application and test your final content and browser requirements.

Original OpenTailwind educational example. Open the full-width demo.
Complete HTML, CSS, and JavaScript

Use your compiled Tailwind stylesheet in place of /examples/tailwind.css. The example uses Tailwind CSS 4 utilities and vanilla JavaScript.

<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,follow"><title>Loading cards without moving the page</title><link rel="stylesheet" href="/examples/tailwind.css"><style>body{font-family:system-ui,sans-serif;background:#faf9f6;color:#252824;padding:24px}button{cursor:pointer}button:disabled{cursor:wait;opacity:.6}:focus-visible{outline:3px solid #4f46e5;outline-offset:4px}</style></head><body><button id="load" class="mb-5 rounded-lg bg-stone-900 px-4 py-2 text-white">Load example cards</button>
<p id="status" role="status" class="mb-4 text-sm text-stone-600">Ready to load.</p>
<section id="results" aria-label="Example projects" aria-busy="false" class="grid grid-cols-1 gap-4 sm:grid-cols-3"></section><script>const results = document.querySelector('#results');
const status = document.querySelector('#status');
const button = document.querySelector('#load');
const titles = ['A quiet workspace', 'A new perspective', 'Made for the everyday'];
const cards = () => titles.map((title, i) => '<article class="h-56 rounded-xl border bg-white p-4"><div class="mb-4 h-28 rounded-lg bg-orange-100"></div><h2 class="font-semibold">' + title + '</h2><p class="mt-2 text-sm text-stone-600">Project ' + (i + 1) + '</p></article>').join('');
results.innerHTML = cards();
button.addEventListener('click', async () => {
  button.disabled = true;
  results.setAttribute('aria-busy', 'true');
  status.textContent = 'Loading three projects…';
  results.innerHTML = titles.map(() => '<div aria-hidden="true" class="h-56 animate-pulse rounded-xl border bg-white p-4 motion-reduce:animate-none"><div class="mb-4 h-28 rounded-lg bg-stone-200"></div><div class="h-5 w-3/4 rounded bg-stone-200"></div><div class="mt-3 h-4 w-1/2 rounded bg-stone-100"></div></div>').join('');
  // Simulated delay only. Replace with a request and handle its failure.
  await new Promise(resolve => setTimeout(resolve, 1400));
  results.innerHTML = cards();
  results.setAttribute('aria-busy', 'false');
  status.textContent = 'Three projects loaded.';
  button.disabled = false;
});</script></body></html>

The demo deliberately uses a simulated request so you can inspect the transition. The card shell keeps a fixed height in both states. In a real page, use dimensions that accommodate your longest title rather than copying this height blindly. Avoid inserting extra banners above the results when the request completes.

Choose the right loading treatment

SituationUseful treatmentAvoid
Known card gridMatching card skeletonsRandom line lengths that bear no relation to the final cards
Data tableStable column widths and a few placeholder rowsReplacing the entire page with a spinner
Saving a formDisabled submit control and a status messageHiding the entered values
Unknown durationA clear progress message; percentage only if measuredInvented progress percentages

Make loading understandable without the animation

The decorative shapes have aria-hidden="true". A separate live status explains that projects are loading and then reports completion. The results container uses aria-busy, which is reset when the operation finishes. Keeping the status outside that busy region avoids delaying the message with the content it describes.

Do not create one live region per card. Repeated announcements make a page difficult to use. Keep keyboard focus on the initiating control unless the user explicitly requested navigation to new content. The demo follows the status-message approach described by W3C’s status-message guidance.

Adapt the shape for tables and dashboards

For a table, preserve the header and column structure. Show placeholder cells inside the existing table instead of creating a different grid that later changes width. Use a short explanatory caption or status outside the busy table. For a dashboard, load sections independently when one slow request should not block the rest of the page.

At mobile widths, test the real card count and column change. A three-column desktop skeleton followed by a one-column result makes the loading transition unnecessarily disruptive. Reuse the same responsive grid classes around both states.

Handle errors, empty results, and reduced motion

The example uses motion-reduce:animate-none to keep the shapes still when reduced motion is preferred. The content must remain understandable without pulsing. Use the same principle for any shimmer effect.

In production, wrap the request in try/catch/finally. Treat an HTTP error as an error even if fetch resolves. Clear the busy state, preserve useful content, and provide a retry control. A successful response containing no items should say “No projects yet,” not leave the skeleton running.

Before shipping

  • Try a fast response, a slow response, an empty response, and a failed response.
  • Check the transition with reduced motion enabled.
  • Confirm that long content does not overlap the reserved space.
  • Verify that loading and completion are announced without moving focus.

Continue with success notifications, or explore feature layouts that need a consistent loading state.

Sources and further reading

  • Tailwind animation utilities
  • Tailwind motion preference variants
Library access: Tutorials and their educational demos are public. Copying library designs requires an account: 100 selected blocks and five templates per industry are free. Pro lifetime access unlocks the complete library.

Keep building

Responsive Tailwind CSS Navbar: Working Menu and 5 Layouts ↗

Try a keyboard-operable responsive navbar, copy its HTML and JavaScript, and adapt five navigation layouts with clear implementation notes.

Why Your Dropdown Is Hidden: CSS z-index and Overflow Fixes ↗

Diagnose clipped dropdowns, stacking contexts, and viewport collisions with an interactive example and a practical fix checklist.

Tailwind CSS Success Notifications: Inline Status or Toast? ↗

Create a working success notification example with accessible status updates, honest save feedback, and practical toast guidance.

Browse 1,761 Tailwind CSS blocks

100 free blocks, five free templates per industry, and Pro lifetime access to the complete block and template library.

Browse BlocksTemplates
← Previous article

shadcn/ui Alternatives: Choose by Stack and Ownership

Next article →

Why Your Dropdown Is Hidden: CSS z-index and Overflow Fixes

OpenTailwindv1.0

A little less starting from scratch.
A lot more making it yours.

Made with care by lindo.ai

The library

All blocksAll templatesHero sectionsPricing sections

Around here

Our storyGuides & ideasDesign examplesMembership & pricingQuestions?

Good design is a
good place to start.

FREE TO EXPLORE. YOURS TO BUILD.
© 2026 OpenTailwind · by lindo.ai
TermsPrivacyCookies All creativity welcome