In Playwright, how do you restrict page.locator('button') to only the visible buttons?
answer
- Bounding box plus a computed style check
- A method shortcut arrived in 1.63
- Checked at use, not at call
- Zero size and display none excluded
- Transparent is not the same as hidden
basics
~20 sCall locator.visible(), added in Playwright 1.63, which returns a locator matching only the visible elements. The older :visible CSS pseudo-class does the same inside a selector string. Visible means a non-empty bounding box and no visibility:hidden.
solid answer
~40 sChain `locator.visible()`: `page.locator('button').visible()` returns a new locator matching only the visible buttons. It was added in Playwright 1.63 and is now the recommended form, in preference to writing the `:visible` pseudo-class inside a CSS string as `page.locator('button:visible')`. Both use the same definition: an element is visible when it has a non-empty bounding box and its computed style is not `visibility:hidden`. So `display:none` and zero-size elements are excluded, while `opacity:0` counts as visible. Crucially the check runs **every time the locator is used**, not when `visible()` was called, so a button that appears later is picked up on the next action. Use it for genuinely duplicated markup, not to silence an ambiguous selector.
code
typescript · 10 lines// Issue detail renders the composer twice: sidebar and inline.
const composer = page.locator('form.composer').visible();
await composer.click();
// Equivalent using the older CSS pseudo-class.
await page.locator('form.composer:visible').click();
// Visibility is re-checked on every use, so a later-rendered row works too.
const rowActions = page.locator('.issue-row button').visible();
await rowActions.first().click();go deeper
Remember the definition: non-empty bounding box and no visibility:hidden. Chain visible() onto a locator when a page keeps a hidden duplicate of the element around, and write the method rather than the CSS pseudo-class.
Explain that the filter is part of the locator's description, so it is re-evaluated on every use rather than captured once, and that opacity:0 elements still count as visible under this definition.
Treat a visibility filter added purely to make a match unambiguous as a review finding. Ask what the second match is and whether the test should be scoped to a container instead of trusting the current CSS.
Decide whether responsive duplicate markup is something tests should route around at all, or a signal that the application should render one composer and adapt it. The selector is cheap; the duplicated DOM it works around is not.
## What Playwright means by visible Playwright uses one definition of visibility everywhere: an element is **visible when it has a non-empty bounding box and does not have a `visibility:hidden` computed style**. Everything else follows from that pair of conditions: - An element with `display:none` has no box, so it is not visible. - An element of zero width or height is not visible, even though it is in the DOM. - An element with `visibility:hidden` is not visible, even though it occupies layout space. - An element with `opacity:0` **is** visible by this definition -- it still has a box and its computed `visibility` is not `hidden`. That last case surprises people, and it is worth stating out loud in an interview: visibility here is a layout question, not a "can a human perceive it" question. ## Two ways to narrow a locator to visible elements ```typescript // Preferred, available from Playwright 1.63. page.locator('button').visible() // The older CSS pseudo-class, still supported. page.locator('button:visible') ``` `locator.visible()` returns a **new locator** that matches only the visible elements among the originals. It was added in Playwright 1.63 and the documentation now names it as the recommended way to distinguish elements by visibility, in preference to the `:visible` pseudo-class. | | `locator.visible()` | `:visible` pseudo-class | |---|---|---| | Where it lives | a method on the locator | inside a `css=` selector string | | Works after `getByRole` etc. | yes, it chains onto any locator | no, it needs a CSS string | | Typo protection | a method name the compiler checks | a string the compiler ignores | | Available since | Playwright 1.63 | long-standing | ## Evaluated on use, not on call The important mechanic: **visibility is checked every time the locator is used, not at the moment `visible()` is called.** A locator is a description of how to find elements, not a snapshot of them. So this sequence does what you want: 1. Build `const rowActions = page.locator('.issue-row button').visible()` while the board is still loading. 2. The board finishes rendering and two previously hidden buttons appear. 3. The next action or assertion on `rowActions` re-runs the query and sees the buttons that are visible **at that moment**. The same is true of `button:visible` -- the filter rides along with every re-resolution. ## When it helps, and when it hides a bug Narrowing by visibility genuinely helps where a page keeps duplicate markup around: a mobile and a desktop version of the same toolbar, a modal template that lives in the DOM until opened, a collapsed panel in an issue detail view. In those cases the invisible copy is noise and filtering it out is the honest fix. It becomes a smell when it is used to paper over ambiguity. If a locator matches several elements and you reach for `.visible()` purely to make the error go away, you have made the test pass without learning why the page contains more matches than you expected -- and you have written a test that starts targeting a different element the day the hidden copy becomes visible. Prefer scoping to a container, or locating the element by something that distinguishes it semantically. ## In an issue tracker An issue detail page renders the comment composer twice: once in the sidebar for wide viewports and once inline for narrow ones, with CSS deciding which is shown. `page.locator('form.composer')` matches both. `page.locator('form.composer').visible()` matches the one the current viewport is actually showing, which is exactly the element a user would type into -- and it keeps working when the test project switches viewport size, because the check is re-run on every use.
- Is an element with opacity:0 visible to Playwright?Yes. The definition is a non-empty bounding box plus a computed style that is not `visibility:hidden`, and a fully transparent element satisfies both. If you need to exclude it, assert on the style or on a class the application sets, because the visibility filter will not do it for you.
- Does calling visible() freeze the set of matched elements at that moment?No. A locator describes how to find elements rather than holding them, so the visibility check is re-run each time the locator is used. Building the locator before the page has finished rendering is fine; the next action or assertion evaluates against the DOM as it stands then.
- When is filtering by visibility the wrong fix?When you reach for it only because a selector matched more elements than expected. That hides the real question -- why does the page contain extra matches -- and produces a test that silently retargets the day the hidden copy is shown. Scope to a container or locate by something semantically distinguishing instead.
saying these in an interview costs you the question
- Believing opacity:0 elements are treated as hidden
- Thinking visibility is captured once when the locator is built
- Assuming an element off screen is automatically not visible
- Using a visibility filter to silence an ambiguous selector
- Claiming visibility:hidden elements count because they occupy space