Resource library

QA How-To

How to Use Playwright hover (2026)

Learn playwright hover with reliable TypeScript examples for menus, tooltips, coordinates, touch alternatives, fast debugging, and CI-ready assertions.

22 min read | 3,144 words

TL;DR

Use await locator.hover() on a stable locator, then verify the resulting menu, tooltip, or style with a web-first assertion. Playwright handles scrolling and actionability, while position, modifiers, trial, and force cover specialized cases.

Key Takeaways

  • Use await locator.hover() for element-aware mouse movement with scrolling and actionability checks.
  • Assert the visible result because a completed hover does not prove that the menu or tooltip appeared.
  • Prefer role, label, and test-id locators over DOM-coupled CSS or XPath selectors.
  • Use position only for spatial controls, and reserve page.mouse for pointer paths and canvas behavior.
  • Diagnose overlays and animation with trial mode, headed runs, and traces before increasing timeouts.
  • Test keyboard and touch alternatives so essential actions are not available only through hover.

The Playwright hover API is Locator.hover(), an action that moves the browser mouse over an element after Playwright checks that the target is ready for interaction. In most tests, the reliable pattern is to locate the trigger by role or label, call await trigger.hover(), then assert the visible result with a web-first assertion.

Hover behavior looks simple, but real applications add delayed menus, CSS transitions, overlays, tooltips, pointer coordinates, and touch alternatives. This guide explains the exact API, shows runnable TypeScript tests, and gives you a debugging method that avoids sleeps and fragile selectors.

TL;DR

Need Recommended API Why
Open a hover menu await locator.hover() Includes locator resolution, scrolling, and actionability checks
Hover a specific hotspot locator.hover({ position: { x, y } }) Uses coordinates relative to the element padding box
Hold a modifier locator.hover({ modifiers: ['Shift'] }) Produces the intended modified pointer interaction
Check readiness only locator.hover({ trial: true }) Runs actionability checks without moving the pointer
Verify the outcome expect(result).toBeVisible() Retries until the UI reaches the expected state
Diagnose a failure Playwright trace viewer Preserves actions, DOM snapshots, console, and network evidence

A strong test expresses three things: the element receiving the pointer, the user-visible effect, and the state proving success. Avoid waitForTimeout because a fixed delay neither proves that hover worked nor adapts to application timing.

1. What Playwright Hover Does

Locator.hover() moves the mouse over the matching element. Before the action, Playwright waits for the locator to resolve to one element and performs the actionability checks used for pointer actions. The target must be visible and stable, and it must receive pointer events at the action point. Playwright scrolls the element into view when needed, then moves the mouse to the target.

The default point is the center of the element. If the application attaches behavior to a smaller region, pass a position relative to the element padding box. Hover can activate CSS :hover rules and normal browser pointer or mouse event handlers. It does not directly set a CSS state or call application JavaScript, so the test stays close to a real desktop interaction.

import { test, expect } from '@playwright/test';

test('reveals account actions on hover', async ({ page }) => {
  await page.goto('https://example.com/account');

  const accountMenu = page.getByRole('button', { name: 'Account' });
  await accountMenu.hover();

  await expect(page.getByRole('menu')).toBeVisible();
  await expect(page.getByRole('menuitem', { name: 'Sign out' })).toBeVisible();
});

The awaited hover finishes when the pointer action has been dispatched. It does not promise that every animation or later network request has completed. Assert the observable result so Playwright can wait for the state that matters.

2. Set Up a Runnable Playwright Hover Test

Start with a standard Playwright Test project. These commands install the test package, download browser binaries, and run a TypeScript spec:

npm init playwright@latest
npx playwright test

A minimal configuration can test Chromium, Firefox, and WebKit without duplicating code:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  use: {
    baseURL: 'http://127.0.0.1:3000',
    trace: 'on-first-retry'
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } }
  ]
});

The test should navigate to a known route, establish required state, hover a meaningful control, and verify the result. Use baseURL so the test remains portable across local and CI environments.

import { test, expect } from '@playwright/test';

test('product card exposes quick view', async ({ page }) => {
  await page.goto('/catalog');

  const card = page.getByRole('article', { name: /noise cancelling headphones/i });
  await card.hover();

  const quickView = card.getByRole('button', { name: 'Quick view' });
  await expect(quickView).toBeVisible();
  await quickView.click();

  await expect(page.getByRole('dialog', { name: 'Product details' })).toBeVisible();
});

Scoping Quick view to the card prevents strict-mode failures when the catalog contains the same action on every product. For more locator detail, see the Playwright locator strategy guide. Keep setup deterministic. A hover test built on random inventory, reused accounts, or an uncontrolled banner can fail before reaching the behavior it intends to cover.

3. Playwright Hover Syntax and Options

The TypeScript form is await locator.hover(options). Most tests need no options. Add one only when it represents product behavior or helps isolate a readiness problem.

Option Example Appropriate use
position { position: { x: 12, y: 8 } } Hover a defined region inside a large target
modifiers { modifiers: ['Shift'] } Exercise behavior that depends on held keys
force { force: true } Bypass nonessential actionability checks for an exceptional UI
trial { trial: true } Wait until the action would be possible without moving the pointer
timeout { timeout: 5_000 } Set a focused upper bound for this action

Here is a complete example using a modifier and position:

import { test, expect } from '@playwright/test';

test('shows precision data for a modified chart hover', async ({ page }) => {
  await page.goto('/analytics');

  const chart = page.getByTestId('revenue-chart');
  await chart.hover({
    position: { x: 160, y: 80 },
    modifiers: ['Shift']
  });

  const tooltip = page.getByRole('tooltip');
  await expect(tooltip).toContainText('Revenue');
  await expect(tooltip).toContainText('Compare');
});

Coordinates are relative to the element, not the viewport. They become brittle if responsive layout or data changes the semantic meaning of the point, so use them only for spatial controls such as charts, maps, canvases, and image hotspots.

Trial mode is useful when an animation must settle before another operation. It performs actionability checks but skips the hover:

await page.getByRole('button', { name: 'Products' }).hover({ trial: true });

Force is not a general fix for timeouts. It can conceal an overlay, disabled transition, or wrong locator. First collect evidence and correct the state.

4. Choose a Locator That Survives UI Change

Hover reliability begins with the locator. Prefer a role and accessible name when the target is interactive. Use text for stable visible content, label for form controls, and a deliberate test id when no user-facing contract identifies a canvas, icon-only region, or generated visualization.

const helpButton = page.getByRole('button', { name: 'Help' });
const pricingCard = page.getByRole('article', { name: 'Professional plan' });
const chart = page.getByTestId('latency-chart');

Avoid selectors describing implementation details:

// Fragile: tied to nesting and generated class names.
const trigger = page.locator('div.nav > div:nth-child(3) .css-17a9');

A locator should identify the intended trigger before hover state exists. Do not include :hover in the selector passed to hover because the element cannot match until the pointer is already there. If multiple nodes share the same label, scope through a stable parent instead of reaching for first():

const navigation = page.getByRole('navigation', { name: 'Primary' });
const products = navigation.getByRole('button', { name: 'Products' });
await products.hover();
await expect(navigation.getByRole('menu', { name: 'Products' })).toBeVisible();

This model reads like the interface and fails clearly when accessibility semantics change. It also makes strict locator behavior useful, since multiple matches expose ambiguity instead of silently selecting the wrong element. Locator chains are evaluated when the action runs, so they work with applications that rerender the trigger before interaction.

5. Verify Menus, Tooltips, and CSS Hover State

Do not treat a successful hover call as proof that the feature works. Assert the user-facing outcome. For a navigation menu, verify visibility and a meaningful option:

const solutions = page.getByRole('button', { name: 'Solutions' });
await solutions.hover();

const menu = page.getByRole('menu', { name: 'Solutions' });
await expect(menu).toBeVisible();
await expect(menu.getByRole('menuitem', { name: 'For QA teams' })).toBeVisible();

For a tooltip, prefer its accessible role or content. A well-implemented tooltip usually becomes associated with its trigger through accessible description semantics:

const info = page.getByRole('button', { name: 'Billing information' });
await info.hover();

await expect(page.getByRole('tooltip')).toHaveText(
  'Invoices are generated on the first day of each month.'
);

For a pure CSS change, assert a computed property only when that visual state is the actual requirement:

const card = page.getByRole('article', { name: 'Starter plan' });
await card.hover();

await expect(card).toHaveCSS('transform', 'matrix(1, 0, 0, 1, 0, -4)');

Computed CSS assertions can be browser-sensitive, especially for colors, transitions, and shorthand properties. Prefer a visible action or label when one exists. If appearance is the contract, a screenshot assertion can be appropriate after disabling or completing motion. Learn the broader synchronization model in Playwright auto-waiting explained. Web-first assertions repeatedly inspect the locator until they pass or time out, exactly what delayed hover content needs.

6. Handle Delays, Animations, and Disappearing Content

Hover interfaces often include two delays: a short opening delay to avoid accidental activation and a closing delay that lets the pointer travel into a submenu. Fixed sleeps imitate neither requirement reliably.

Hover the trigger, assert the panel, then continue through locators:

test('opens a nested documentation menu', async ({ page }) => {
  await page.goto('/');

  await page.getByRole('button', { name: 'Documentation' }).hover();

  const menu = page.getByRole('menu', { name: 'Documentation' });
  await expect(menu).toBeVisible();

  await menu.getByRole('menuitem', { name: 'API reference' }).hover();
  await expect(page.getByRole('menu', { name: 'API reference' })).toBeVisible();
});

If the submenu disappears while the mouse crosses a real gap, that can be a product defect. A desktop user has the same path. Do not automatically force the action or execute JavaScript to keep the panel open.

When animation blocks pointer events, wait on an observable state. An application might expose aria-expanded or data-state:

const trigger = page.getByRole('button', { name: 'Workspace' });
await trigger.hover();
await expect(trigger).toHaveAttribute('aria-expanded', 'true');
await expect(page.getByRole('menu', { name: 'Workspace' })).toBeVisible();

You can reduce motion with the reducedMotion emulation setting when animation is not under test. Keep a focused test for important motion-dependent behavior. If a test repeatedly loses hover because an element rerenders, rely on the locator to resolve the current node and investigate whether the application briefly replaces the target with a noninteractive copy.

7. Use Position, Mouse, and Bounding Boxes Correctly

Locator.hover() is the default because it combines locating, actionability, scrolling, and pointer movement. Page.mouse is lower level. Use it for paths, canvas exploration, or behavior tied to pointer travel rather than one target.

test('updates a canvas crosshair along a path', async ({ page }) => {
  await page.goto('/charts');

  const canvas = page.getByTestId('latency-canvas');
  const box = await canvas.boundingBox();
  if (!box) throw new Error('Latency canvas is not visible');

  await page.mouse.move(box.x + 20, box.y + 30);
  await page.mouse.move(box.x + 120, box.y + 70, { steps: 8 });

  await expect(page.getByTestId('crosshair-value')).not.toHaveText('');
});

The null check matters because boundingBox returns null when an element is not visible. Viewport coordinates can change after scrolling or layout updates, so calculate the box immediately before moving and avoid unrelated actions between measurement and use.

Approach Built-in waiting Coordinate system Best fit
locator.hover() Yes Element center Menus, cards, buttons, tooltips
locator.hover({ position }) Yes Element-relative Charts, maps, hotspots
page.mouse.move() No locator actionability Main-frame CSS pixels Pointer paths and canvas behavior
dispatchEvent('mouseover') No real pointer movement Not applicable Narrow event-handler testing only

dispatchEvent can fire a DOM event, but it bypasses hit testing and does not reproduce a real pointer. It should not replace normal hover coverage. If you use it for a component-level edge case, document why browser pointer behavior is outside that test scope.

8. Test Desktop and Touch Behavior Deliberately

Hover is primarily a mouse or trackpad interaction. Mobile and touch-first users may never produce the same state. A responsive product should expose critical actions through tap, focus, or always-visible controls rather than making them hover-only.

Keep desktop hover tests in desktop projects. Add a touch project for the alternative behavior:

import { test, expect, devices } from '@playwright/test';

test.use({ ...devices['iPhone 13'] });

test('opens account navigation with a tap on touch', async ({ page }) => {
  await page.goto('/');

  await page.getByRole('button', { name: 'Account' }).tap();

  await expect(page.getByRole('menu', { name: 'Account' })).toBeVisible();
});

Do not call hover in a touch test merely to make hidden controls accessible. That confirms an automation mechanism, not the mobile journey. Decide the interaction contract first: desktop hover may open a preview, keyboard focus may reveal the same content, and touch may use a tap or disclosure button.

Keyboard accessibility deserves a focused test:

const trigger = page.getByRole('button', { name: 'Help' });
await trigger.focus();
await expect(page.getByRole('tooltip')).toBeVisible();

A tooltip communicating essential information should work with focus as well as hover. Combining desktop, keyboard, and touch scenarios gives better confidence than repeating the identical pointer action across device profiles where it does not represent actual input.

9. Debug When Playwright Hover Is Not Working

Start with the error. A timeout while waiting for visibility suggests the trigger never became actionable. A completed hover followed by a missing tooltip suggests application state, timing, wrong target, or an outcome locator issue.

Use this sequence:

  1. Run one test headed: npx playwright test tests/hover.spec.ts --headed.
  2. Open Inspector with PWDEBUG=1 npx playwright test tests/hover.spec.ts.
  3. Enable trace on the first retry and inspect the action, DOM snapshot, console, and network.
  4. Check how many elements the locator matches and whether the intended element is visible.
  5. Inspect overlays, sticky headers, cookie banners, and animation at the action point.
  6. Confirm that the result appears for a real mouse in the same environment.
  7. Repeat to expose timing patterns: npx playwright test tests/hover.spec.ts --repeat-each=20.

A useful diagnostic is trial hover:

const trigger = page.getByRole('button', { name: 'Features' });
await trigger.hover({ trial: true });
await trigger.hover();
await expect(page.getByRole('menu', { name: 'Features' })).toBeVisible();

If trial times out, the issue is target readiness or hit testing, not the menu assertion. If trial succeeds and the result never appears, inspect event handling and outcome selection. The Playwright timeout troubleshooting guide explains test, action, navigation, and assertion timeouts. Increase a timeout only after evidence shows valid behavior is predictably slower.

10. Playwright Hover Patterns for a Test Suite

A mature suite keeps hover details close to component or page behavior. A small component object can expose intent without hiding assertions:

import { expect, type Locator, type Page } from '@playwright/test';

export class HeaderNavigation {
  readonly root: Locator;

  constructor(page: Page) {
    this.root = page.getByRole('navigation', { name: 'Primary' });
  }

  async openMenu(name: string): Promise<Locator> {
    await this.root.getByRole('button', { name }).hover();
    const menu = this.root.getByRole('menu', { name });
    await expect(menu).toBeVisible();
    return menu;
  }
}

The test remains readable:

test('opens the enterprise page from Solutions', async ({ page }) => {
  const header = new HeaderNavigation(page);
  const menu = await header.openMenu('Solutions');

  await menu.getByRole('menuitem', { name: 'Enterprise' }).click();
  await expect(page).toHaveURL(/\/enterprise$/);
});

Keep the helper contract narrow. It should not add arbitrary sleeps, catch timeouts, use force by default, or select first() to suppress ambiguity. Let failures retain the action log and call-site context.

For repeated tooltip tests, use data-driven cases only when behavior is identical. Give every row a human-readable name and verify exact content. For visual states, control fonts, data, viewport, and reduced motion before screenshot comparison. The Playwright visual testing guide covers snapshot stability. Treat hover as one step in a user outcome, not a goal by itself.

A final review checklist

Before merging a hover test, review it as a user journey. Confirm that the trigger has a stable identity, the target is actually reachable by pointer, and the assertion names the required outcome. Run the case with keyboard input or a touch project when the content is essential. Verify that animation, delayed closing, and responsive layout do not change the intended contract.

Review diagnostics too. A failure should retain a useful trace, show which component was targeted, and avoid exposing sensitive data in screenshots or attachments. Run the focused case repeatedly after changing synchronization, then run it in each supported browser project. If the only way to make the test pass is force, event injection, or a long sleep, return to the product behavior and actionability evidence. A stable test should model the supported interaction and fail clearly when that interaction stops working.

Interview Questions and Answers

Q: What is the preferred way to hover an element in Playwright?

Use a Locator and await locator.hover(). This approach resolves the current DOM element, scrolls it into view, and performs pointer actionability checks. Follow it with a web-first assertion for the resulting menu, tooltip, or visual state.

Q: Why is locator.hover() preferable to page.mouse.move() for most controls?

Locator.hover() is element-aware and includes waiting, hit testing, and scrolling. Page.mouse.move() accepts viewport coordinates and does not know whether the intended element is actionable. Mouse movement is appropriate when pointer path or raw coordinates are under test.

Q: How do you hover a specific point inside an element?

Pass position with x and y values to locator.hover(). Coordinates are relative to the element padding box. Use this for spatial widgets, and avoid it for ordinary buttons where the center is the correct target.

Q: Does hover wait for a tooltip to appear?

It waits for target actionability and dispatches pointer movement, but it does not know the application's success condition. Assert the tooltip with expect(locator).toBeVisible() or toHaveText() so Playwright retries until the expected UI appears.

Q: When is force appropriate with hover?

Force is a narrow escape hatch when you intentionally need to bypass nonessential actionability checks and understand the tradeoff. It should not be the first response to an overlay or animation failure. Diagnose the obstruction first.

Q: How would you test hover accessibility?

Test the pointer path, add a keyboard scenario using focus or Tab, and assert the same essential content. Add a touch-specific path when the feature is available on mobile. Critical information should not be reachable only through hover.

Common Mistakes

  • Calling hover on a selector containing :hover. Locate the element in its normal state, then perform the action.
  • Adding waitForTimeout after every hover. Assert the outcome with a retrying assertion.
  • Using page.mouse coordinates for a regular button. Prefer locator.hover for actionability and scrolling.
  • Applying force to every timeout. Force can hide a real overlay, wrong selector, or unusable interface.
  • Selecting first() when many cards match. Scope to the intended region or identify the card by accessible name.
  • Verifying only that hover returned. Assert the menu, tooltip, CSS state, navigation, or action users observe.
  • Ignoring keyboard and touch. Cover the alternative interaction where hover is unavailable.
  • Reusing stale bounding-box coordinates after layout changes. Measure immediately before mouse movement.
  • Hiding all hover logic inside a broad utility. Preserve meaningful names, assertions, and diagnostics.

Conclusion

Use Playwright hover through a stable Locator, let actionability checks protect the interaction, and assert the state users can see. Add position or low-level mouse movement only when the product is genuinely spatial, and treat force as an investigated exception.

Choose one hover menu, one delayed tooltip, and one touch alternative from your application. Implement those three focused tests, run them across intended browser projects, and review a trace so your team understands the failure evidence.

Interview Questions and Answers

How do you perform a mouse hover in Playwright?

I locate the trigger with a resilient Locator and await locator.hover(). I then assert the user-visible effect with a web-first assertion. This separates successful pointer delivery from successful application behavior.

What checks does Playwright perform before hover?

For a normal pointer action, Playwright resolves the locator and checks that the target is visible, stable, and able to receive events at the action point. It scrolls the element into view before moving the mouse. These checks make locator.hover() more reliable than raw coordinates.

How would you test a delayed tooltip?

I hover the trigger and assert the tooltip with toBeVisible or toHaveText. The assertion retries through the intended delay, so I do not add a fixed sleep. I also verify the keyboard focus path if the tooltip contains essential information.

When would you use the position option?

I use position when different regions inside one element have different meaning, such as chart points, maps, and image hotspots. The coordinates are element-relative. For ordinary controls, the default center point is clearer and less fragile.

When would you use page.mouse instead of locator.hover?

I use page.mouse when movement itself matters, for example crossing a canvas, testing a pointer trail, or moving through coordinates. It lacks locator actionability checks, so I validate geometry carefully. For menus and buttons, I prefer locator.hover().

What does trial mode do for hover?

Trial mode performs actionability checks without carrying out the hover. It helps separate target-readiness failures from application response failures. It should remain a diagnostic or deliberate synchronization tool, not a ritual before every hover.

Why can force make a hover test misleading?

Force bypasses nonessential actionability checks, so the test may interact through a condition that blocks a real user. An overlay or wrong stacking state could remain unfixed while automation passes. I first inspect the trace and target geometry.

How do you make hover tests work with repeated cards?

I identify the intended card by accessible name or stable data and scope the trigger and result within that card. I avoid first() because it turns ambiguity into a dependency on DOM order. Strict locator failures then remain useful feedback.

Frequently Asked Questions

How do I hover over an element in Playwright?

Create a Locator for the element and call await locator.hover(). Then assert the effect, such as a visible tooltip or menu, with a Playwright web-first assertion.

Does Playwright hover wait for the element?

Yes. Locator.hover() waits for the locator to resolve and for relevant pointer actionability checks, including visibility, stability, and receiving events. It does not automatically wait for the application hover result, so assert that state separately.

How do I hover at specific coordinates in Playwright?

Pass a position object, for example locator.hover({ position: { x: 40, y: 20 } }). The coordinates are relative to the element padding box, which makes the option useful for charts, maps, and hotspots.

Why is Playwright hover not working?

Common causes include an ambiguous locator, an overlay intercepting pointer events, unstable animation, a wrong action point, or an incorrect result locator. Use a headed run, trial hover, and trace viewer to determine whether the action or expected outcome failed.

Should I use force with Playwright hover?

Use force only after confirming that bypassing actionability matches the test intent. Applying it broadly can hide genuine usability problems such as a cookie banner, sticky header, or transparent overlay covering the target.

Can Playwright hover over a hidden element?

A normal user cannot hover a hidden element, and default actionability checks will reject it. Test the interaction that makes the element visible first, or correct the product state rather than forcing an unrealistic pointer action.

What is the difference between locator.hover and page.mouse.move?

Locator.hover() targets an element and provides waiting, scrolling, and actionability checks. page.mouse.move() uses main-frame CSS pixel coordinates and is better for pointer paths, canvas interactions, or movement between points.

Related Guides