Files
cv-site/HTMX-INDICATORS-FIX-REPORT.md
T
juanatsap 25e9ebafe7 bf fixes
2025-11-16 10:11:58 +00:00

7.5 KiB

HTMX Loading Indicators - Fix Report

Date: 2025-11-15 Issue: HTMX loading indicators exist but never become visible Status: FIXED


Root Cause Analysis

Problem Identified

The loading indicators were destroyed mid-animation because they were children of elements being replaced by HTMX swap operations.

Original Structure (BROKEN):

<div class="language-selector" id="language-selector">
    <button hx-get="/switch-language?lang=en"
            hx-target="#language-selector"    <!-- SWAPS THIS DIV -->
            hx-swap="outerHTML">
        <span>English</span>
        <iconify-icon class="htmx-indicator">  <!-- GETS DESTROYED! -->
        </iconify-icon>
    </button>
</div>

Timeline of Failure:

  1. User clicks button
  2. HTMX adds .htmx-request class to button
  3. CSS starts opacity transition: 0 → 1
  4. HTMX swap replaces #language-selector (includes the button!)
  5. Indicator element destroyed at ~7ms into 200ms transition
  6. Opacity reaches only 0.003 before destruction
  7. New button rendered without .htmx-request class

Evidence

From Playwright timeline monitoring:

Time 585ms: htmx-request=true, opacity=0.000000
Time 592ms: htmx-request=false, opacity=0.003076  ← Transitioning but...
Time 600ms+: opacity=NaN                          ← Element destroyed!

Solution Implemented

1. Restructure HTML - Move Indicators Outside Swap Target

File: templates/partials/navigation/language-selector.html

New Structure:

<div class="language-selector-wrapper">
    <!-- Indicators OUTSIDE the swap target -->
    <iconify-icon id="lang-indicator-en"
                  class="htmx-indicator spinning small light">
    </iconify-icon>
    <iconify-icon id="lang-indicator-es"
                  class="htmx-indicator spinning small light">
    </iconify-icon>

    <!-- Swap target - buttons get replaced, indicators persist -->
    <div class="language-selector" id="language-selector">
        <button hx-get="/switch-language?lang=en"
                hx-target="#language-selector"
                hx-indicator="#lang-indicator-en">  <!-- Points to external indicator -->
            <span>English</span>
        </button>
        <button hx-get="/switch-language?lang=es"
                hx-target="#language-selector"
                hx-indicator="#lang-indicator-es">
            <span>Español</span>
        </button>
    </div>
</div>

Key Changes:

  • Indicators moved OUTSIDE #language-selector div
  • Each button uses hx-indicator="#id" to point to its indicator
  • Indicators persist during swap operation
  • HTMX adds .htmx-request to the indicator itself (not the button)

File: templates/language-switch.html

Updated swap response to match new structure (removes indicators from buttons).

2. Update CSS

File: static/css/main.css

Added Wrapper Styles:

/* Language selector wrapper - contains indicators outside swap target */
.language-selector-wrapper {
    position: relative;
    display: inline-flex;
    height: 100%;
}

/* Position language indicators next to their respective buttons */
#lang-indicator-en,
#lang-indicator-es {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    pointer-events: none;
    z-index: 10;
}

/* Position indicators inside the button visual area */
#lang-indicator-en {
    left: calc(1rem + 50px); /* Inside first button */
}

#lang-indicator-es {
    left: calc(1rem + 135px); /* Inside second button */
}

Fixed CSS Specificity Issue:

/* Added !important to ensure opacity change takes precedence */
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
    opacity: 1 !important;  /* ← Added !important */
}

Removed Conflicting Display Rule:

/* BEFORE: Conflicted with base styles */
.htmx-indicator.spinning {
    display: inline-block;  /* ← REMOVED THIS */
    animation: htmx-spin 1s linear infinite;
}

/* AFTER: Inherits display: inline-flex from .htmx-indicator */
.htmx-indicator.spinning {
    animation: htmx-spin 1s linear infinite;
}

Added Iconify Override:

/* Ensure iconify-icon indicators override global iconify-icon display style */
iconify-icon.htmx-indicator {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

How It Works Now

HTMX Behavior with External Indicators

When using hx-indicator="#id" to point to an external element:

  1. User clicks button
  2. HTMX adds .htmx-request class to the INDICATOR (not the button)
  3. CSS rule .htmx-request.htmx-indicator { opacity: 1 !important; } triggers
  4. Indicator transitions from opacity: 0 to opacity: 1 (200ms)
  5. HTMX swap replaces button (indicator PERSISTS outside swap target)
  6. Request completes
  7. HTMX removes .htmx-request from indicator
  8. Indicator transitions back to opacity: 0

CSS Cascade Order

Critical: The !important flag is necessary because there are multiple .htmx-indicator rules:

  1. Line 193: .htmx-indicator { flex-shrink: 0; } - No conflict
  2. Line 503: .htmx-indicator { opacity: 0; ... } - Base hidden state
  3. Line 513: iconify-icon.htmx-indicator { display: inline-flex; } - Override global iconify
  4. Line 521: .htmx-request.htmx-indicator { opacity: 1 !important; } - SHOW INDICATOR
  5. Line 585: Media query for reduced motion - No conflict

Without !important, later rules or specificity conflicts could override the visibility.


Verification Steps

Manual Testing (Browser)

  1. Open http://localhost:1999/?lang=en
  2. Open DevTools → Network tab
  3. Throttle network to "Slow 3G"
  4. Click "Español" button
  5. EXPECTED: Spinning loader appears next to button during request
  6. VERIFY: Opacity transitions from 0 to 1, spinner rotates

Automated Testing (Playwright)

// Test confirms:
// 1. Indicators exist in DOM
// 2. Initial opacity = 0
// 3. During HTMX request: .htmx-request class added to indicator
// 4. Opacity transitions to 1
// 5. Indicator visible and spinning
// 6. After request: opacity returns to 0

See /tmp/test-htmx-behavior.js for full test.


Files Modified

  1. templates/partials/navigation/language-selector.html - Restructure HTML
  2. templates/language-switch.html - Update swap response
  3. static/css/main.css - Fix CSS specificity and positioning

Lessons Learned

HTMX Swap Target Considerations

  • Problem: Indicators inside elements being swapped get destroyed mid-animation
  • Solution: Place indicators OUTSIDE swap targets, use hx-indicator attribute
  • Key Insight: HTMX adds .htmx-request to the indicator when using external reference

CSS Specificity & Cascade

  • Problem: Multiple .htmx-indicator rules can conflict
  • Solution: Use !important for visibility rule, remove conflicting display properties
  • Key Insight: CSS loaded order matters even with same specificity

Testing Methodology

  • Problem: Fast local requests complete before indicators are visible
  • Solution: Use network throttling or delays to observe transitions
  • Key Insight: Playwright timeline monitoring reveals exact opacity values over time

Status

FIXED: HTMX loading indicators now properly display during language switch requests

Next Steps:

  • Apply same pattern to toggle controls if they have similar swap issues
  • Add E2E tests with network throttling to verify indicators
  • Consider adding visible feedback for very fast requests (min-duration CSS or JS)

Debugging Surgeon | 2025-11-15