# 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):
```html
<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**:
```html
<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**:
```css
/* 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**:
```css
/* 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**:
```css
/* 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**:
```css
/* 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)

```javascript
// 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
