An htmx extension for displaying skeleton screens during requests.
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/htmx-ext-skeleton"></script> npm install htmx-ext-skeletonThen import in your JavaScript:
import 'htmx.org';
import 'htmx-ext-skeleton';- Define a skeleton template with
id="skeleton":
<script type="text/template" id="skeleton">
<div class="skeleton-placeholder">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
</div>
</script>- Add
hx-ext="skeleton"to your htmx element:
<div hx-ext="skeleton" hx-get="/api/data" hx-target="#content">
Load Data
</div>
<div id="content">
<!-- Initial content here -->
</div>If you need multiple different skeletons, specify a custom template using any CSS selector:
<script type="text/template" id="custom-skeleton">
<div class="custom-loading">...</div>
</script>
<div hx-ext="skeleton"
hx-get="/api/data"
hx-target="#content"
hx-skeleton="#custom-skeleton">
Load Data
</div>Use hx-skeleton-target to display the skeleton in a different element than the swap target:
<button hx-ext="skeleton"
hx-get="/api/data"
hx-target="#results"
hx-skeleton-target="#loading-area">
Load Data
</button>
<div id="loading-area">
<!-- Skeleton appears here -->
</div>
<div id="results">
<!-- Data swaps here -->
</div>If hx-skeleton-target is not specified, the extension falls back to hx-target, and if that's not present, it uses htmx's default target (the element itself).
If Alpine.js is detected, you can override and extend data in your skeleton template using hx-skeleton-alpine:
<script type="text/template" id="skeleton">
<div x-data="{ title: 'Loading...', count: 3 }">
<h3 x-text="title"></h3>
<template x-for="i in count" :key="i">
<div class="skeleton-item"></div>
</template>
</div>
</script>
<div hx-ext="skeleton"
hx-get="/api/data"
hx-target="#content"
hx-skeleton-alpine='{"title": "Loading Projects", "count": 5}'>
Load Data
</div>- Zero configuration: Just add
hx-ext="skeleton"and create a template withid="skeleton" - Instant feedback: Shows skeleton immediately when request starts
- Automatic cleanup: Removes skeleton when new content arrives
- Error handling: Restores original content if request fails
- History support: Properly handles browser back/forward navigation
- Multiple skeletons: Use
hx-skeletonwith any CSS selector for custom templates - Custom targets: Use
hx-skeleton-targetto display skeleton in a different element than the swap target - Alpine.js support: Optional integration with Alpine.js for dynamic skeleton templates
-
When an htmx request starts (
htmx:beforeRequest), the extension:- Saves the original content
- Replaces it with the skeleton template
- Adds a
skeleton-loadingclass
-
When the response arrives (
htmx:beforeSwap), the extension:- Removes the
skeleton-loadingclass - Allows htmx to swap in the new content
- Removes the
-
If an error occurs, the extension:
- Restores the original content
- Removes the
skeleton-loadingclass
Add CSS to style your skeleton screens:
.skeleton-loading {
pointer-events: none;
opacity: 0.7;
}
.skeleton-line {
height: 1rem;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
margin-bottom: 0.5rem;
border-radius: 4px;
}
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}- Breaking/Enhancement:
hx-skeletonnow accepts any CSS selector instead of just an ID (defaults to#skeletonfor backward compatibility) - New Feature: Added
hx-skeleton-targetattribute to specify a different target for skeleton display - Enhancement: Improved target resolution - falls back to
hx-target, then htmx default ifhx-skeleton-targetnot specified
- Fixed issue that could cause skeleton to re-appear when navigating back in browser
- Added tests
- Updated Alpine.js integration to require x-data initialization within skeleton template
- Added optional Alpine.js support to skeleton templates
MIT