52 lines
2 KiB
Markdown
52 lines
2 KiB
Markdown
---
|
|
title: "React useState Dropdown — CSS group-hover vs useState for Playwright"
|
|
source: daily/2026-05-10.md
|
|
updated: 2026-05-10
|
|
tags: [react, playwright, testing, css, dropdown, hover]
|
|
---
|
|
|
|
# React useState Dropdown — CSS `group-hover:` vs `useState` for Playwright
|
|
|
|
## Problem
|
|
|
|
Playwright's `hover()` on a trigger element does not reliably open a React dropdown that uses `useState` to toggle visibility:
|
|
|
|
```tsx
|
|
// React component
|
|
const [open, setOpen] = useState(false)
|
|
|
|
return (
|
|
<div onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
|
|
<button>Menu</button>
|
|
{open && <div className="dropdown">...</div>} {/* conditional render */}
|
|
</div>
|
|
)
|
|
```
|
|
|
|
`page.hover('[data-testid="menu-trigger"]')` causes the dropdown to appear and immediately disappear before the next Playwright action can interact with it. The issue is a React render-cycle lag: `hover()` fires `mouseenter`, React schedules a re-render, but `mouseleave` may fire before the re-render commits (especially in headless Chrome).
|
|
|
|
## Fix: Pure CSS Hover
|
|
|
|
Replace `useState` toggle with CSS `group-hover:` (Tailwind) or `:hover` pseudo-class:
|
|
|
|
```tsx
|
|
// No useState needed
|
|
return (
|
|
<div className="group relative">
|
|
<button>Menu</button>
|
|
<div className="hidden group-hover:block absolute ...">
|
|
...dropdown content...
|
|
</div>
|
|
</div>
|
|
)
|
|
```
|
|
|
|
CSS hover is handled by the browser's rendering engine synchronously — no React render cycle involved. Playwright `hover()` reliably keeps the element in the hover state for subsequent actions.
|
|
|
|
## Tradeoff
|
|
|
|
CSS `group-hover:` doesn't support keyboard navigation (`:focus-within` can supplement this). For production components that need full accessibility, use a headless UI library (Radix UI, Headless UI) which manages hover state internally with proper ARIA.
|
|
|
|
## When `useState` Is Required
|
|
|
|
If the dropdown must stay open after a click (not just hover), `useState` is appropriate. In Playwright, use `click()` instead of `hover()` in that case, and add a small `waitFor` after click if needed.
|