5.9 KiB
Skeleton Loader Bug Fix - Verification Report
🔴 BUG IDENTIFIED
Issue: Skeleton loader was stuck permanently visible after language switch
ROOT CAUSE ANALYSIS
The Problem
The hyperscript event handlers were attached to the #language-selector element, which gets completely replaced during HTMX swap:
<!-- BEFORE (BROKEN) -->
<div class="language-selector-wrapper">
<div class="language-selector" id="language-selector"
_="on htmx:beforeRequest from .selector-btn
add .active to #skeleton-loader
end
on htmx:afterSwap from .selector-btn
wait 100ms
remove .active from #skeleton-loader
end">
<button hx-target="#language-selector"
hx-swap="outerHTML">...</button>
</div>
</div>
What happened:
- ✅ User clicks language button
- ✅
htmx:beforeRequestfires → skeleton appears (.activeadded) - ❌ HTMX swaps entire
#language-selectorwith outerHTML → Event handlers DESTROYED - ❌
htmx:afterSwapfires, but no listener exists on new element - ❌ Skeleton stuck with
.activeclass forever
The Solution
Move hyperscript handlers to the parent wrapper that doesn't get swapped:
<!-- AFTER (FIXED) -->
<div class="language-selector-wrapper"
_="on htmx:beforeRequest from .selector-btn
add .active to #skeleton-loader
end
on htmx:afterSwap from .selector-btn
wait 100ms
remove .active from #skeleton-loader
end">
<div class="language-selector" id="language-selector">
<button hx-target="#language-selector"
hx-swap="outerHTML">...</button>
</div>
</div>
Why this works:
- ✅ Event handlers on
.language-selector-wrapper(persists across swaps) - ✅ Listens for events FROM
.selector-btn(event bubbling) - ✅
htmx:beforeRequest→ skeleton appears - ✅ HTMX swaps
#language-selector→ wrapper remains intact - ✅
htmx:afterSwap→ wrapper handlers still exist → skeleton disappears
FILES MODIFIED
-
templates/partials/navigation/language-selector.html
- Moved hyperscript from
#language-selectorto.language-selector-wrapper
- Moved hyperscript from
-
templates/language-switch.html
- Removed duplicate hyperscript from swapped element
VERIFICATION STEPS
1. HTML Structure Verification ✅
curl -s http://localhost:1999/ | grep -A 10 "language-selector-wrapper"
Result: Hyperscript correctly attached to wrapper:
<div class="language-selector-wrapper"
_="on htmx:beforeRequest from .selector-btn
add .active to #skeleton-loader
end
on htmx:afterSwap from .selector-btn
wait 100ms
remove .active from #skeleton-loader
end">
2. Swap Response Verification ✅
curl -s "http://localhost:1999/switch-language?lang=es" | grep -A 5 "language-selector"
Result: Inner element has NO hyperscript (as intended):
<div class="language-selector" id="language-selector">
<button class="selector-btn">...</button>
</div>
3. CSS State Verification ✅
curl -s http://localhost:1999/static/css/main.css | grep -A 3 "#skeleton-loader"
Result: Proper CSS states:
#skeleton-loader {
opacity: 0;
pointer-events: none;
transition: opacity 250ms ease-in-out;
}
#skeleton-loader.active {
opacity: 1;
pointer-events: all;
}
MANUAL BROWSER TEST REQUIRED
Test Steps:
- Open http://localhost:1999/?lang=en
- Open DevTools Console
- Run this monitoring script:
// Monitor skeleton loader state
const skeleton = document.getElementById('skeleton-loader');
const observer = new MutationObserver(() => {
console.log('Skeleton classes:', skeleton.className);
console.log('Skeleton opacity:', window.getComputedStyle(skeleton).opacity);
});
observer.observe(skeleton, { attributes: true, attributeFilter: ['class'] });
// Monitor HTMX events
document.body.addEventListener('htmx:beforeRequest', (e) => {
if (e.detail.elt.classList.contains('selector-btn')) {
console.log('[BEFORE] Language switch starting');
}
});
document.body.addEventListener('htmx:afterSwap', (e) => {
if (e.detail.elt.classList.contains('selector-btn')) {
console.log('[AFTER] Language switch complete');
}
});
- Click "Español" button
- Watch console output
Expected Console Output:
[BEFORE] Language switch starting
Skeleton classes: active
Skeleton opacity: 1
[AFTER] Language switch complete
(after 100ms)
Skeleton classes:
Skeleton opacity: 0
Expected Visual Behavior:
- ✅ Skeleton appears immediately (fade in 250ms)
- ✅ Page content swaps (250ms swap + 250ms settle)
- ✅ Skeleton disappears after 100ms delay (fade out 250ms)
- ✅ Total: ~850ms smooth transition
What to Check:
- ✅ Skeleton appears when clicking language button
- ✅ Skeleton disappears after content loads
- ✅ Skeleton does NOT stay stuck visible
- ✅ Can switch languages multiple times without issues
- ✅ Smooth fade in/out transitions
TECHNICAL DETAILS
Event Bubbling
Hyperscript uses from .selector-btn which listens for events that bubble up from any element matching .selector-btn, even if those elements are replaced.
Timing Breakdown
[0ms] User clicks button
[0ms] htmx:beforeRequest → skeleton.active = true
[0ms] Skeleton starts fading in (opacity 0→1 over 250ms)
[100ms] Server responds
[100ms] HTMX starts swap
[350ms] Swap complete (250ms swap duration)
[350ms] htmx:afterSwap fired
[450ms] 100ms wait complete
[450ms] skeleton.active = false
[450ms] Skeleton starts fading out (opacity 1→0 over 250ms)
[700ms] Skeleton fully hidden
STATUS
- ✅ Root cause identified
- ✅ Fix implemented
- ✅ HTML structure verified
- ✅ CSS states verified
- ⏳ Manual browser test REQUIRED
Next Step: Run manual browser test to confirm skeleton loader shows and hides correctly.