Handling hover states on mobile and touch devices in Tailwind CSS 4

July 9, 2025
9 views

Overview - The change in Tailwind CSS 4

Tailwind CSS 4 introduced a breaking change to how hover states work. As stated in their upgrade guide:

In v4 we've updated the hover variant to only apply when the primary input device supports hover. This can create problems if you've built your site in a way that depends on touch devices triggering hover on tap.

What this means: Hover effects now only work on devices with true hover capability (like desktops with mice), not on touch devices where hover was previously triggered by tapping. This can cause issues on devices such as tablets, where for example, you have a menu with dropdown items that relied on hover to open the menu.

Option 1: Quick fix - restore version 3 behavior

To set the behaviour back to how it was in Tailwind CSS version 3, just add the following @custom-variant to your global CSS file:

/* Tailwind CSS 4 - eg. main.css */

@import "tailwindcss";

@custom-variant hover (&:hover);

Pros: Immediate fix, maintains existing functionality
Cons: Doesn't follow modern best practices, may cause "stuck" hover states

Option 2: More complex solution

According to the Tailwind documentation:

Generally though we recommend treating hover functionality as an enhancement, and not depending on it for your site to work since touch devices don't truly have the ability to hover.

To follow Tailwind CSS v4’s recommendation of treating hover as a progressive enhancement, you should design your UI to work without hover effects, and only use them to enhance interactivity where supported.

Tailwind v4 introduced a smarter default behavior: it only generates hover styles inside a @media (hover: hover) block — which means they’ll only apply on devices that support hover (like a desktop). This prevents “stuck” hover states on touch devices like iPads.

For example, dropdown menus should be designed to open on click/tap by default, and optionally enhanced with hover on desktop.

A Vue JS example:


<template>
  <div
    ref="dropdownRef"
    class="relative inline-block text-left"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
  >
    <button
      @click="toggle"
      class="inline-flex justify-center w-full px-4 py-2 bg-gray-200 rounded-md hover:bg-gray-300"
    >
      Menu
    </button>
    <div
      v-if="open"
      class="absolute left-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10"
    >
      <a href="#" class="block px-4 py-2 hover:bg-gray-100">Item 1</a>
      <a href="#" class="block px-4 py-2 hover:bg-gray-100">Item 2</a>
      <a href="#" class="block px-4 py-2 hover:bg-gray-100">Item 3</a>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'

const open = ref(false)
const dropdownRef = ref(null)

const toggle = () => {
  open.value = !open.value
}

// Close on outside click
const handleClickOutside = (event) => {
  if (dropdownRef.value && !dropdownRef.value.contains(event.target)) {
    open.value = false
  }
}

onMounted(() => {
  document.addEventListener('click', handleClickOutside)
})

onBeforeUnmount(() => {
  document.removeEventListener('click', handleClickOutside)
})

// Optional hover enhancement
const canHover = window.matchMedia('(hover: hover)').matches

const handleMouseEnter = () => {
  if (canHover) open.value = true
}

const handleMouseLeave = () => {
  if (canHover) open.value = false
}
</script>

The example code above:

  • Opens the dropdown on click, which works on all devices.
  • Enhances with hover on desktops/laptops.
  • Prevents dropdowns from getting "stuck" open on iPads.
  • Click outside to close improves usability.

0 Comments


Leave a Comment

Share your questions, thoughts and ideas while maintaining a considerate tone towards others, thank you.

All fields are required - your email address will not be published.