1.8 KiB
| title | source | updated |
|---|---|---|
| React useState Dropdown — CSS group-hover vs useState for Playwright | daily/2026-05-10.md | 2026-05-10 |
Problem
Playwright hover() on a trigger element does not reliably open a React dropdown that uses useState for visibility:
// ❌ Playwright hover misses this
const [open, setOpen] = useState(false)
<button onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
Menu
</button>
{open && <DropdownMenu />}
Root cause: useState updates are asynchronous — they schedule a re-render. By the time Playwright checks for the dropdown element, the render cycle may not have completed, so getByRole('menu') returns nothing.
Fix — CSS group-hover:
Replace JS state with a pure-CSS hover using Tailwind's group / group-hover: utilities:
// ✅ Playwright hover works reliably
<div className="group relative">
<button>Menu</button>
<div className="hidden group-hover:block absolute top-full left-0">
<DropdownMenu />
</div>
</div>
The dropdown is always in the DOM; group-hover:block just changes display. No render cycle, no timing issues — Playwright sees the element immediately after hover.
Secondary Benefit
CSS hover-driven dropdowns also fix the overflow: hidden clipping issue — the dropdown is rendered at the right DOM level and can be placed outside the clipping ancestor. See wiki/concepts/overflow-hidden-clips-absolute-children.
When useState Is Still Needed
- Keyboard accessibility (Escape to close, focus trapping)
- Mobile (no hover events) with click-to-toggle
- Complex open/close animations that require knowing state
In those cases, add an explicit await page.waitForSelector('[role=menu]') in Playwright tests rather than relying on hover timing.