Skip to main content

Why Won't My Selected Row Turn Blue? The Hover vs Selection Mystery

· 10 min read
Mahmut Salman
Software Developer

I implemented keyboard navigation for my table, and it worked perfectly—until I tried using the mouse. The selected row would stay gray instead of blue, but only when my mouse was hovering over it. A debugging journey through CSS pseudo-class conflicts.


The Weird Behavior

I'm building a Browse window for my Electron note-taking app. The feature is straightforward:

  • Click a note → highlight with blue background
  • Use arrow keys (↑↓) → navigate between notes
  • The selected note should always show the blue highlight

But something bizarre was happening:

✅ Use arrow keys → Blue background appears immediately
❌ Click with mouse → Gray background, no blue
❌ Keep mouse over row → Still gray... still gray...
✅ Move mouse away → Blue suddenly appears!

Wait, what? The blue background only appears when I move my mouse away?!


The Investigation: Logic vs Visual

My first instinct: "The state isn't updating."

const handleRowClick = (note, index) => {
console.log('Setting selectedIndex to:', index);
setSelectedIndex(index);
};

I added debug logs everywhere:

Console output when clicking row 3:
Setting selectedIndex to: 3 ✅
Component re-rendering... ✅
isSelected for row 3: true ✅
className includes: bg-blue-600/40 ✅

Everything in JavaScript-land is working! The state is updating, the component is re-rendering, the className is correct. But the background is still gray.

This must be a CSS issue.


The Original Code

Here's the JSX that was causing the problem:

<tr
onClick={() => handleRowClick(note, index)}
className={`border-b border-white/5 hover:bg-white/5 cursor-pointer ${
isSelected ? 'bg-blue-600/40' : ''
}`}
>
<td>{note.title}</td>
<td>{note.date}</td>
</tr>

At first glance, this looks reasonable:

  • hover:bg-white/5 → Gray hover effect
  • bg-blue-600/40 → Blue when selected

But let's trace what happens when you click with the mouse:


The Timeline of a Mouse Click

Frame 1: Before Click

Mouse Position: Over row 3
:hover state: ACTIVE ✅
Classes applied: hover:bg-white/5
Background: Gray (from hover)

Frame 2: Click Event

Mouse Position: Still over row 3 (hasn't moved!)
:hover state: STILL ACTIVE ✅
onClick fires: setSelectedIndex(3)
State updates: selectedIndex = 3

Frame 3: Re-render

Mouse Position: Still over row 3
:hover state: STILL ACTIVE ✅
Classes applied: hover:bg-white/5 + bg-blue-600/40
Background: ??? (Both classes are applied!)

Frame 4: CSS Specificity Battle

/* Both of these are applied: */
.hover\:bg-white\/5:hover {
background-color: rgba(255, 255, 255, 0.05);
}

.bg-blue-600\/40 {
background-color: rgba(37, 99, 235, 0.4);
}

Who wins? Let's think about CSS specificity:

hover:bg-white/5 = .hover\:bg-white\/5:hover
→ Class selector + Pseudo-class
→ Specificity: (0, 1, 1)

bg-blue-600/40 = .bg-blue-600\/40
→ Class selector only
→ Specificity: (0, 1, 0)

The :hover pseudo-class gives higher specificity!

So even though both classes are applied, hover:bg-white/5 wins because :hover adds specificity.


Why Keyboard Navigation Works

Now let's trace what happens with arrow key navigation:

Using Arrow Keys

1. Press ↓ arrow key
2. keyDown event handler fires
3. setSelectedIndex(selectedIndex + 1)
4. Component re-renders
5. Mouse might be ANYWHERE (not necessarily over the row)
6. :hover state: INACTIVE ❌
7. Only bg-blue-600/40 applied
8. Background: Blue! ✅

The key difference: The mouse isn't necessarily hovering over the newly selected row!


The Aha Moment

The problem is clear now:

Mouse Click:
→ Mouse IS over the row
→ :hover is active
→ hover:bg-white/5 wins
→ Gray background

Arrow Keys:
→ Mouse is NOT over the row
→ :hover is inactive
→ bg-blue-600/40 wins
→ Blue background

The hover state is overriding the selection state!


The Solution

The fix is simple but elegant: Only apply hover when NOT selected

<tr
onClick={() => handleRowClick(note, index)}
className={`border-b border-white/5 cursor-pointer transition-colors ${
isSelected ? 'bg-blue-600/40' : 'hover:bg-white/5'
}`}
>
<td>{note.title}</td>
<td>{note.date}</td>
</tr>

Now the logic is:

if (isSelected) {
// Apply: bg-blue-600/40
// NO hover class at all!
} else {
// Apply: hover:bg-white/5
// Only when not selected
}

Before vs After

Before (Buggy):

className={`
hover:bg-white/5 ← Applied to ALL rows
${isSelected ? 'bg-blue-600/40' : ''}
`}

Problem: Both classes can be active simultaneously:

/* When mouse is over selected row: */
.hover:bg-white/5:hover { } /* This wins! */
.bg-blue-600/40 { } /* This loses! */

After (Fixed):

className={`
${isSelected
? 'bg-blue-600/40'Selected: Blue, NO hover
: 'hover:bg-white/5'Not selected: Hover works
}
`}

Solution: Classes are mutually exclusive:

/* When mouse is over selected row: */
.bg-blue-600/40 { } /* Only this! */

/* When mouse is over unselected row: */
.hover:bg-white/5:hover { } /* Only this! */

Testing the Fix

After applying the fix, let's test all scenarios:

Test 1: Click with Mouse

1. Click row 3
2. Mouse still over row 3
3. Background: Blue immediately! ✅

Test 2: Hover Over Unselected Row

1. Selected: row 3 (blue)
2. Mouse over row 5 (unselected)
3. Row 5 background: Gray hover ✅
4. Row 3 background: Still blue ✅

Test 3: Arrow Keys

1. Selected: row 3
2. Press ↓ arrow
3. Row 4 background: Blue immediately ✅
4. Row 3 background: Back to transparent ✅

Test 4: Mouse Over Selected Row

1. Selected: row 3 (blue)
2. Move mouse over row 3
3. Background: Stays blue! ✅

All scenarios work perfectly! ✨


The Visual Behavior Comparison

Before (Buggy):

State: Not Selected
Mouse: Away → Transparent ✅
Mouse: Hover → Gray ✅

State: Selected
Mouse: Away → Blue ✅
Mouse: Hover → Gray ❌ (Bug!)

After (Fixed):

State: Not Selected
Mouse: Away → Transparent ✅
Mouse: Hover → Gray ✅

State: Selected
Mouse: Away → Blue ✅
Mouse: Hover → Blue ✅ (Fixed!)

Understanding UI State Hierarchy

This bug reveals an important principle: UI states have a hierarchy.

The Hierarchy

1. Selected/Active     ← Highest priority
2. Focus ← Medium priority
3. Hover ← Lowest priority

Implementation in Code

❌ Wrong: All states applied simultaneously

className={`
hover:bg-gray-100
focus:ring-2
${isSelected ? 'bg-blue-500' : ''}
`}
// Hover can override selection!

✅ Correct: States are mutually exclusive

className={`
${isSelected
? 'bg-blue-500' // No hover/focus when selected
: 'hover:bg-gray-100 focus:ring-2' // Hover/focus only when not selected
}
`}
// Selection always wins!

Real-World Examples

This pattern appears everywhere in UIs:

Example 1: Navigation Menu

<NavLink
className={({ isActive }) =>
isActive
? 'bg-blue-600 text-white' // Selected: solid blue
: 'text-gray-700 hover:bg-gray-100' // Not selected: gray hover
}
>
Dashboard
</NavLink>

Example 2: Button States

<button
className={`
${isActive
? 'bg-blue-600 text-white' // Active: no hover
: 'bg-white hover:bg-gray-50' // Inactive: hover works
}
`}
>
Click me
</button>

Example 3: Table Rows (Our Case)

<tr
className={`
${isSelected
? 'bg-blue-600/40' // Selected: blue
: 'hover:bg-white/5' // Not selected: gray hover
}
`}
>

The pattern: Selection state excludes hover state.


CSS Specificity Deep Dive

Let's understand why the hover was winning:

Specificity Calculation

/* Hover class with pseudo-class */
.hover\:bg-white\/5:hover {
/* Specificity: (0, 1, 1) */
/* = 0 IDs + 1 class + 1 pseudo-class */
}

/* Regular background class */
.bg-blue-600\/40 {
/* Specificity: (0, 1, 0) */
/* = 0 IDs + 1 class + 0 pseudo-classes */
}

Result: (0, 1, 1) > (0, 1, 0) → Hover wins!

Why This Matters

Even though both classes are applied:

<tr class="hover:bg-white/5 bg-blue-600/40">

The browser evaluates:

1. Both classes are present ✓
2. Mouse is hovering ✓
3. Apply hover:bg-white/5 with higher specificity ✓
4. Apply bg-blue-600/40 with lower specificity ✗ (overridden)

Common Variations of This Bug

Variation 1: Multiple States

// ❌ Bug: All states can conflict
<div className={`
hover:bg-gray-100
focus:bg-blue-100
${isActive && 'bg-green-500'}
${isError && 'bg-red-500'}
`}>

// ✅ Fix: Clear priority
<div className={`
${isError
? 'bg-red-500'
: isActive
? 'bg-green-500'
: 'hover:bg-gray-100 focus:bg-blue-100'
}
`}>

Variation 2: Disabled State

// ❌ Bug: Hover works on disabled button
<button className={`
hover:bg-blue-600
${isDisabled && 'bg-gray-300 cursor-not-allowed'}
`}>

// ✅ Fix: No hover when disabled
<button className={`
${isDisabled
? 'bg-gray-300 cursor-not-allowed'
: 'hover:bg-blue-600'
}
`}>

Variation 3: Loading State

// ❌ Bug: Hover visible while loading
<button className={`
hover:bg-blue-700
${isLoading && 'bg-blue-600 cursor-wait'}
`}>

// ✅ Fix: No hover while loading
<button className={`
${isLoading
? 'bg-blue-600 cursor-wait'
: 'hover:bg-blue-700'
}
`}>

Debugging Checklist

When you encounter unexpected hover behavior:

Step 1: Verify State Logic

console.log('State:', { isSelected, isHovered });
// Is the state updating correctly?

Step 2: Inspect Applied Classes

console.log('Classes:', element.className);
// Are both hover and selection classes present?

Step 3: Check CSS Specificity

Open DevTools → Elements → Computed
Look for both styles and see which wins

Step 4: Test Mouse Position

Keep mouse OVER element while clicking
Does behavior change when mouse moves away?

Step 5: Test Keyboard Navigation

Use keyboard to change selection
Does it work correctly without mouse?

The TailwindCSS Pattern

With Tailwind, this pattern is common and easy to implement:

// Pattern: Conditional hover based on state
className={`
${condition
? 'primary-state-classes' // No hover modifiers
: 'default-state hover:...' // Hover only in default state
}
`}

Examples:

// Selected vs not selected
${isSelected ? 'bg-blue-600' : 'hover:bg-gray-100'}

// Active vs inactive
${isActive ? 'text-white bg-blue-600' : 'text-gray-700 hover:text-gray-900'}

// Disabled vs enabled
${isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-700'}

Performance Consideration

Before (Unnecessary Re-renders):

// Hover class always present, even when selected
className={`hover:bg-white/5 ${isSelected ? 'bg-blue-600/40' : ''}`}

Browser still evaluates hover pseudo-class on selected elements (wasted work).

After (Optimized):

// Hover class only when needed
className={isSelected ? 'bg-blue-600/40' : 'hover:bg-white/5'}

Browser only evaluates hover when actually needed.

Performance gain: Minimal but cleaner CSS cascade.


Accessibility Bonus

The fix also improves accessibility:

Before:

Selected row with mouse over:
- Visual: Gray (confusing!)
- Screen reader: "Selected" (correct)
- Visual ≠ Screen reader announcement

After:

Selected row with mouse over:
- Visual: Blue (clear!)
- Screen reader: "Selected" (correct)
- Visual = Screen reader announcement ✓

Visual state now matches semantic state!


Key Takeaways

What We Learned
  1. CSS pseudo-classes have specificity that can override regular classes

  2. UI states have hierarchy:

    • Selection/Active (highest)
    • Focus (medium)
    • Hover (lowest)
  3. Conditional hover pattern:

    ${isSelected ? 'selected-classes' : 'hover:hover-classes'}
  4. Mouse position persists during click events:

    • Click doesn't move mouse
    • :hover stays active
    • Can cause unexpected visual states
  5. Test both input methods:

    • Mouse interactions
    • Keyboard navigation
    • Touch (if applicable)
  6. Debug visual bugs by checking:

    • State logic (JavaScript)
    • Applied classes (DOM)
    • CSS specificity (DevTools)
    • Mouse/focus position
  7. Tailwind best practice: Use conditional className, not layered classes for conflicting states


Conclusion

What seemed like a mysterious bug—"why won't my selected row turn blue?"—had a perfectly logical explanation: CSS pseudo-class specificity.

The hover state was literally overpowering the selection state because :hover adds specificity. The fix was simple: don't apply hover classes to selected elements.

The broader lesson: When working with interactive UIs, always think about state hierarchy. Selection should override hover, not the other way around.

And remember: if your state logic is correct but the visual isn't, it's probably a CSS issue! 🎨


Technical Details

  • Framework: React with Electron
  • CSS Framework: TailwindCSS
  • Issue Type: CSS pseudo-class specificity conflict
  • Solution: Conditional hover class application
  • Lines Changed: 1 line (but what a difference!)

Tags: #css #react #debugging #tailwind #ui-states #electron


This bug was discovered during the development of my Electron note-taking app's browse window. Sometimes the simplest bugs teach the most valuable lessons!