Resource library

QA How-To

How to Use Playwright expect toBeVisible (2026)

Learn playwright expect toBeVisible syntax, visibility rules, retries, timeouts, locators, negative checks, and debugging patterns for stable UI tests.

24 min read | 2,620 words

TL;DR

Use `await expect(locator).toBeVisible()` after the action that should reveal the element. It retries until the locator points to an attached, visible DOM node or the expect timeout expires. It does not guarantee that the element is in the viewport or can receive a click.

Key Takeaways

  • Use await expect(locator).toBeVisible() for a retrying assertion that an attached element has visible rendered presence.
  • Visibility does not prove viewport intersection, clickability, enabled state, opacity, or pixel-level correctness.
  • Prefer semantic, container-scoped locators so the assertion identifies one intended element.
  • Configure realistic assertion timeouts and avoid fixed sleeps or immediate isVisible checks for required states.
  • Use toBeHidden for disappearance and toBeInViewport when viewport intersection is the actual requirement.
  • Add separate visibility assertions only when visibility is an outcome or improves failure diagnosis.
  • Use traces, call logs, cardinality checks, and computed styles to diagnose persistent failures.

Playwright expect toBeVisible is the web-first assertion for proving that a locator resolves to an attached DOM element with a visible rendered box. It retries until the element becomes visible or the assertion timeout expires, so it is usually safer and more informative than reading locator.isVisible() and asserting the returned boolean.

Use await expect(locator).toBeVisible() after the action that should reveal the element. The assertion is ideal for headings, dialogs, status messages, panels, controls, and other required UI states. This guide covers exact semantics, timeouts, locator design, responsive duplicates, negative checks, debugging, and interview-ready explanations.

TL;DR

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

test('shows the account panel', async ({ page }) => {
  await page.goto('/account');
  await page.getByRole('button', { name: 'Open profile' }).click();

  const panel = page.getByRole('region', { name: 'Profile' });
  await expect(panel).toBeVisible();
});
Question Answer
Does toBeVisible() retry? Yes, until it passes or its assertion timeout ends
Must you use await? Yes
Does it prove the element is in the viewport? No, use toBeInViewport() for that
Does it prove the element can receive a click? No, click actionability performs additional checks
Is opacity zero considered hidden? No, Playwright visibility semantics still treat it as visible
Should you use isVisible() to wait? No, it is an immediate boolean query
What is the default assertion timeout? The configured expect.timeout, 5 seconds by default

1. What Does toBeVisible Mean in Playwright?

toBeVisible() is a locator assertion provided by Playwright Test. It succeeds when the locator points to an attached element that Playwright considers visible. At a practical level, the element needs a non-empty bounding box and must not have visibility: hidden. An element with display: none has no rendered box and is not visible.

Several details surprise otherwise experienced automation engineers. An element with opacity: 0 is considered visible under Playwright's definition. An offscreen element can also be visible, because visibility and viewport intersection are different properties. A visible element can still be covered by another element and unable to receive a pointer event.

That means toBeVisible() answers one focused question: did the expected element obtain a visible rendered presence? It does not prove clickability, legibility, accessibility, or full viewport exposure.

The assertion works with a Locator, which Playwright re-resolves while retrying. This is important for React, Vue, and other applications that replace nodes during rendering. You assert the logical locator, not a cached element handle.

Use visibility as a user-facing state contract. If opening a dialog should reveal a "Confirm deletion" heading, assert that heading or named dialog. Do not assert an incidental CSS class unless the class itself is the requirement.

2. Playwright expect toBeVisible Syntax and Runnable Setup

Import test and expect from @playwright/test. Create a locator, perform the event that should reveal it, and await the assertion.

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

test('reveals shipping options', async ({ page }) => {
  await page.setContent(`
    <button type="button" aria-controls="shipping">Choose shipping</button>
    <section id="shipping" aria-label="Shipping options" hidden>
      <h2>Shipping options</h2>
      <label><input type="radio" name="speed"> Express</label>
    </section>
    <script>
      const button = document.querySelector('button');
      const section = document.querySelector('section');
      button.addEventListener('click', () => {
        section.hidden = false;
      });
    </script>
  `);

  const options = page.getByRole('region', { name: 'Shipping options' });
  await expect(options).toBeHidden();

  await page.getByRole('button', { name: 'Choose shipping' }).click();

  await expect(options).toBeVisible();
  await expect(options.getByRole('radio', { name: 'Express' })).toBeVisible();
});

This test is runnable without an external server. The first assertion establishes the initial state, the click creates the transition, and the final assertions validate its visible result.

You can pass an assertion-specific timeout:

await expect(page.getByRole('status')).toBeVisible({ timeout: 10_000 });

You can also add a custom message as the second argument to expect:

await expect(
  page.getByRole('dialog', { name: 'Payment confirmation' }),
  'payment confirmation dialog should open after submit',
).toBeVisible();

The message appears in Playwright reporting and gives the failure a business context.

3. How Auto-Retry Works

A web-first locator assertion does not take one visibility snapshot. It repeatedly resolves the locator and evaluates the matcher until the condition passes or the timeout ends. This makes the assertion suitable for UI states that emerge after rendering, animation, network data, or scheduled application work.

Suppose a status message appears 600 milliseconds after a form submission. The assertion waits for that condition without a fixed sleep:

await page.getByRole('button', { name: 'Save changes' }).click();
await expect(page.getByRole('status')).toBeVisible();
await expect(page.getByRole('status')).toHaveText('Changes saved');

The first assertion proves visible presence. The text assertion proves content and also implies that the locator resolves to the expected visible DOM text in the relevant state. Sometimes the second assertion alone is enough. Include both when visibility is an explicit requirement or when separate failure messages improve diagnosis.

Do not replace this with:

expect(await page.getByRole('status').isVisible()).toBe(true);

isVisible() returns immediately. If rendering has not completed at that instant, the test fails even though the application would become correct a moment later. Adding page.waitForTimeout() merely guesses how long rendering might take.

The Playwright auto-waiting guide explains how locator assertions and actionability waits address different stages of interaction.

4. Visibility Compared With Related Assertions

Choose the matcher that describes the requirement, not the one that happens to pass.

Matcher or query What it establishes Typical use
toBeVisible() One resolved element is attached and visible Dialog, heading, panel, control appears
toBeHidden() Element is absent or non-visible Spinner, drawer, or notice disappears
toBeAttached() Element is connected to a document or shadow root Mounted component may remain visually hidden
toBeInViewport() Element intersects the viewport by a required ratio Lazy content or scroll-position requirement
toHaveCount(n) Locator resolves to exactly n nodes Collection size or absence with count zero
locator.isVisible() Immediate visibility boolean Optional branch, not an expected wait
locator.waitFor({ state: 'visible' }) Waits for visible state without an assertion Low-level infrastructure helper

Visibility is not existence. A hidden tab panel can be attached but not visible. Visibility is also not viewport intersection. A footer far below the fold may have a non-empty box and be visible even before scrolling.

Visibility is not click actionability. A cookie dialog can cover a visible "Continue" button. toBeVisible() passes for the button, while click() waits for additional conditions such as receiving events, stability, and enabled state.

For locator strategy fundamentals, use the Playwright locators best practices guide.

5. Configure Assertion Timeouts Correctly

The assertion uses the timeout from TestConfig.expect unless you override it for that call. The default is 5 seconds. Configure a suite-wide value in playwright.config.ts only when it reflects the application's normal readiness budget.

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

export default defineConfig({
  timeout: 30_000,
  expect: {
    timeout: 5_000,
  },
  use: {
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
});

The test timeout and assertion timeout are separate. A 20-second visibility assertion cannot reliably consume its full budget if the surrounding test has only a few seconds left. Likewise, increasing the test timeout does not automatically change the configured expect timeout.

Use a local override for a known slow boundary:

await expect(
  page.getByRole('heading', { name: 'Generated report' }),
).toBeVisible({ timeout: 20_000 });

Do not use a long timeout for an ordinary menu or modal. A slow local interaction often indicates a missing click, wrong locator, overlay, failed request, or broken product state.

Timeouts do not slow successful assertions because they return as soon as the condition passes. They do make real failures slower. Tune for the expected state transition, not the slowest historical CI incident.

The Playwright timeout troubleshooting guide helps separate assertion waits from action and test timeouts.

6. Build a Locator That Identifies One Visible Target

A high-quality visibility assertion starts with a high-quality locator. Prefer role and accessible name, associated label, user-visible text, or an intentional test ID. Scope repeated names to a meaningful region.

const dialog = page.getByRole('dialog', { name: 'Edit profile' });
await expect(dialog).toBeVisible();

const saveButton = dialog.getByRole('button', { name: 'Save changes' });
await expect(saveButton).toBeVisible();

A locator that matches multiple elements can create ambiguity. If a list locator is intentional and the requirement is that at least one item is visible, select an explicit element such as first(). If one unique control is expected, improve the locator instead of hiding the duplicate.

const alerts = page.getByRole('alert');
await expect(alerts.first()).toBeVisible();

Do not use first() merely to silence a strictness problem. DOM order may change, and a hidden responsive copy may come first. Scope to an open dialog, active navigation, product card, or named form.

For example, locate a product card by its heading before checking its button:

const card = page.getByRole('article').filter({
  has: page.getByRole('heading', { name: 'Mechanical Keyboard' }),
});

await expect(card.getByRole('button', { name: 'Add to cart' })).toBeVisible();

The assertion now represents the intended product, not any matching button on the page.

7. Handle Responsive and Hidden Duplicate Elements

Responsive applications sometimes keep desktop and mobile components in the DOM and hide one with CSS. Dialog libraries may leave closed content mounted. A broad locator can therefore match visible and hidden copies of the same text.

Start by checking cardinality during diagnosis:

const menuLinks = page.getByRole('link', { name: 'Settings' });
console.log('settings matches:', await menuLinks.count());

Then express the active region:

const mobileNavigation = page.getByRole('navigation', {
  name: 'Mobile primary',
});
await expect(mobileNavigation).toBeVisible();

const settings = mobileNavigation.getByRole('link', { name: 'Settings' });
await expect(settings).toBeVisible();

Set deterministic viewports through Playwright projects when the scenario targets a device class. Do not choose nth(1) based on current DOM order unless order is an explicit contract.

If the product offers one of two legitimate entry points, locator composition can express the alternative, but consider whether both could appear and create multiple matches. A direct conditional based on isVisible() is appropriate only when the branch is genuinely optional and already settled. It is not a replacement for waiting on a required UI transition.

Hidden duplicate failures often reveal accessibility problems too. Named regions and unique accessible controls make both testing and user navigation clearer.

8. Negative Visibility: toBeHidden vs not.toBeVisible

For a disappearing element, toBeHidden() is usually the clearest positive expression of the expected hidden state:

const spinner = page.getByRole('progressbar', { name: 'Loading orders' });

await page.getByRole('button', { name: 'Refresh' }).click();
await expect(spinner).toBeVisible();
await expect(spinner).toBeHidden();
await expect(page.getByRole('heading', { name: 'Orders' })).toBeVisible();

toBeHidden() passes when the locator resolves to a non-visible element or no matching element. That is often correct for a spinner because products may either hide or remove it.

expect(locator).not.toBeVisible() also expresses the negative of visible, but negative assertions can be harder to read. Choose the form that makes the business condition obvious and keep team style consistent.

Do not assert absence with an immediate count:

expect(await page.getByRole('dialog').count()).toBe(0);

Use a retrying matcher when the DOM changes asynchronously:

await expect(page.getByRole('dialog')).toHaveCount(0);

Be precise about the requirement. If an element must remain mounted for animation or accessibility but become hidden, toBeHidden() captures the visible result, while toHaveCount(0) would incorrectly require removal.

9. Visibility Is Not Viewport, Opacity, or Clickability

A sound test distinguishes browser rendering concepts.

State toBeVisible() may pass? Better additional check
Element is below the fold Yes toBeInViewport() if viewport presence matters
Element has opacity: 0 Yes Product-specific CSS or screenshot check if required
Element is covered by an overlay Yes Perform the intended action and let actionability check
Element has display: none No None, it is not visible
Element has zero width or height No Inspect layout and parent state
Element has visibility: hidden No Inspect reveal transition
Element is disabled Yes toBeEnabled() if interaction requires it

This distinction prevents false conclusions. If a click times out after toBeVisible() passed, the assertion did not fail. The control may be unstable, disabled, outside an active layer, or unable to receive pointer events.

Likewise, visibility does not prove useful visual appearance. Transparent text or a white icon on a white background can satisfy structural visibility. Use screenshot comparison when pixel presentation is the requirement, with deliberate masking and stable rendering controls.

The Playwright visual regression testing guide covers image-based assertions without confusing them with DOM visibility.

10. Wait for the Business State, Not Every Element

Adding toBeVisible() before every action makes tests verbose and can duplicate actionability waiting. A normal click() already waits for relevant actionability conditions. Add a separate visibility assertion when visibility itself is the outcome or when its failure message improves the scenario.

Good:

await page.getByRole('button', { name: 'Open cart' }).click();
await expect(page.getByRole('dialog', { name: 'Shopping cart' })).toBeVisible();

Usually redundant:

const submit = page.getByRole('button', { name: 'Submit' });
await expect(submit).toBeVisible();
await submit.click();

The second form can still be valuable if the requirement explicitly states that a submit control appears before it is enabled, or if you want separate visibility and enabled diagnostics. Otherwise the click is the stronger behavioral operation.

Prefer the business-ready signal over a generic container. A report page may render its shell immediately but populate the result later. Asserting the result heading, row count, or final status can describe readiness better than checking a wrapper div.

Tests should communicate state transitions: action, visible outcome, content or behavior outcome. They should not become a list of defensive waits.

11. Debug a Failing toBeVisible Assertion

Read the assertion call log first. It shows the locator, timeout, and observed states. Then inspect the trace around the triggering action. A screenshot, DOM snapshot, network panel, and source step can reveal whether the element never appeared or the locator selected the wrong node.

Use focused diagnostics:

const target = page.getByRole('dialog', { name: 'Checkout' });

console.log({
  count: await target.count(),
  visibleNow: await target.isVisible(),
});

if (await target.count() > 0) {
  console.log(await target.first().evaluate((element) => ({
    html: element.outerHTML,
    rect: element.getBoundingClientRect().toJSON(),
    display: getComputedStyle(element).display,
    visibility: getComputedStyle(element).visibility,
    opacity: getComputedStyle(element).opacity,
  })));
}

This code is for diagnosis, not the final assertion. isVisible() is useful here because you intentionally want a current snapshot.

Investigate in this order:

  1. Did the action that reveals the element happen and complete?
  2. Does the locator match the intended node?
  3. Are there duplicate desktop, mobile, or closed-dialog copies?
  4. Did a request, permission, feature flag, or test record change the state?
  5. Is a parent hidden or zero-sized?
  6. Did the component render under a different accessible name?
  7. Does the trace show a product defect?

Do not fix the failure with a sleep, force, or CSS mutation. Those tactics either guess timing or alter the user experience being tested.

12. Playwright expect toBeVisible 2026 Review Checklist

Use this checklist during test and page object review:

  • Import expect from @playwright/test.
  • Await every locator assertion.
  • Trigger the state change before asserting its visible outcome.
  • Prefer semantic, container-scoped locators.
  • Ensure a single-target expectation resolves intentionally.
  • Use toBeInViewport() when viewport intersection is the actual requirement.
  • Use toBeEnabled() or the action itself when interaction readiness matters.
  • Use toBeHidden() for disappearance rather than immediate boolean checks.
  • Set timeout at the narrowest level that reflects the real readiness window.
  • Avoid redundant assertions before actions.
  • Retain traces and screenshots for CI failures.
  • Diagnose duplicate markup and hidden ancestors before changing selectors.
  • Use a screenshot assertion for pixel-level appearance.
  • Confirm final business state, not only visibility.

A negative test is useful when creating a helper. Keep the target hidden and confirm the assertion times out with a readable locator and custom message. Then reveal it and confirm the test passes without a fixed delay.

Interview Questions and Answers

Q: What does toBeVisible() assert in Playwright?

It asserts that a locator points to an attached DOM node that Playwright considers visible. The assertion retries until it passes or reaches its configured timeout.

Q: What is the difference between toBeVisible() and isVisible()?

toBeVisible() is a retrying test assertion with diagnostics. isVisible() returns an immediate boolean and is suitable for settled optional branching or debugging, not waiting for required state.

Q: Does visible mean clickable?

No. Click actionability also considers stability, enabled state, and whether the target receives events. An overlay can cover an element that still satisfies visibility.

Q: Does toBeVisible() require the element to be in the viewport?

No. Use toBeInViewport() when viewport intersection is the requirement. An offscreen element can have a visible rendered box.

Q: How is opacity zero treated?

Playwright's visibility semantics consider an element with opacity: 0 visible. If transparency is the defect under test, use a computed-style or visual assertion that directly expresses it.

Q: How do you handle two matching elements when only one is visible?

I scope the locator to the active semantic region or improve its accessible name. I use first() only when selecting the first matching item is intentional, not to hide duplicate markup.

Q: How do assertion timeouts work?

The matcher uses expect.timeout from configuration unless a call-specific timeout is supplied. It returns immediately on success, and the surrounding test timeout still limits the overall test.

Q: When is a separate visibility assertion redundant?

It is often redundant immediately before a click because the click performs actionability waiting. I keep it when visible appearance is itself the expected outcome or separate diagnosis adds value.

Common Mistakes

  • Forgetting to await toBeVisible().
  • Using isVisible() plus a hard value assertion to wait for rendering.
  • Assuming visible means inside the viewport.
  • Assuming visible means the element can receive a click.
  • Expecting opacity: 0 to fail the visibility assertion.
  • Selecting a hidden responsive duplicate with a broad locator.
  • Adding first() without understanding duplicate matches.
  • Raising global timeouts instead of finding the missing state transition.
  • Adding a visibility assertion before every action.
  • Using fixed sleeps before visibility checks.
  • Requiring count zero when the product intentionally keeps a hidden node mounted.
  • Mutating CSS in the test to force the assertion to pass.

Conclusion

Playwright expect toBeVisible should describe a meaningful UI outcome: the expected locator becomes an attached, visibly rendered element within a realistic assertion timeout. Its retrying behavior makes it the default choice for required visibility transitions.

Keep the semantics narrow. Visibility is not viewport presence, clickability, opacity, or visual correctness. Choose the matching assertion for each requirement, use semantic locators, and debug the actual state transition when visibility never arrives.

Interview Questions and Answers

Explain Playwright's toBeVisible assertion.

It is a web-first locator assertion that retries until the locator points to an attached element with visible rendered presence. It uses the configured expect timeout and provides a detailed call log on failure.

How does toBeVisible differ from locator.waitFor?

Both can wait for visible state, but `toBeVisible()` expresses a test expectation and integrates matcher diagnostics. I reserve `locator.waitFor()` mainly for lower-level helpers where an assertion is not the right abstraction.

Can a covered element pass toBeVisible?

Yes. Coverage affects whether the target receives pointer events, which is a click actionability concern. Visibility alone does not test unobstructed interaction.

How would you debug a toBeVisible timeout?

I read the call log, inspect locator count and current visibility, then open the trace around the reveal action. I check duplicates, accessible names, hidden ancestors, layout size, requests, permissions, and feature state.

Why are semantic locators important for visibility assertions?

They connect the assertion to the control or region a user recognizes and reduce accidental matches against hidden templates. Container scoping also distinguishes repeated names in dialogs, navigation, and responsive layouts.

When would you use toBeInViewport instead?

I use it when the requirement concerns scrolling, lazy loading, or a defined viewport intersection. An element can satisfy `toBeVisible()` while remaining below the fold.

Should every click have a preceding toBeVisible assertion?

No. Click already performs visibility and other actionability waits. I add the assertion only when visibility is a distinct expected outcome or a separate failure message materially improves diagnosis.

How do you test a spinner that disappears?

If initial appearance is required, I first assert it is visible. Then I use `toBeHidden()` and assert a positive ready-state element, because spinner absence alone may not prove the business result loaded.

Frequently Asked Questions

How do I use expect toBeVisible in Playwright?

Create a locator and write `await expect(locator).toBeVisible()`. Perform the action that reveals the element first, and optionally pass `{ timeout: milliseconds }` for a legitimate slower state.

Does Playwright toBeVisible wait automatically?

Yes. It re-resolves the locator and retries the visibility condition until it passes or its assertion timeout ends. The default assertion timeout is controlled by `expect.timeout`.

What is the difference between toBeVisible and isVisible?

`toBeVisible()` is a retrying assertion that produces Playwright diagnostics. `isVisible()` returns an immediate boolean, so it should not be used as a waiting strategy for expected UI changes.

Does toBeVisible mean an element is clickable?

No. A visible element can be covered, moving, or disabled. A click performs additional actionability checks, including stability and receiving pointer events.

Does toBeVisible require an element to be in the viewport?

No. An offscreen element may still be visible. Use `toBeInViewport()` when the requirement is that some defined portion intersects the viewport.

Why does opacity zero pass toBeVisible?

Playwright's visibility definition treats opacity-zero elements as visible. Test opacity with a CSS or visual assertion if transparency is the product requirement.

How do I assert that a Playwright element disappears?

Use `await expect(locator).toBeHidden()` when either hidden or removed is acceptable. Use `toHaveCount(0)` only when the element must be absent from the DOM.

Related Guides