How to Get an Element by ID in Playwright
Select elements by HTML id in Playwright with #id or id=, then use reliable locators, assertions, frames, and debugging techniques.
Use a Playwright Locator with a CSS ID selector:
const saveButton = page.locator('#save-button');
await saveButton.click();
You can also use Playwright’s explicit ID selector engine:
const saveButton = page.locator('id=save-button');
await saveButton.click();
Both forms select an element whose HTML id attribute is save-button. The returned Locator can be reused for actions and assertions; Playwright locators auto-wait and retry as the page changes. See the official Locator API and other locator engines.
Basic example
<button id="save-button">Save</button>
import { test, expect } from '@playwright/test';
test('saves a document', async ({ page }) => {
await page.goto('https://example.com/editor');
const saveButton = page.locator('#save-button');
await expect(saveButton).toBeVisible();
await saveButton.click();
});
The CSS form, #save-button, is concise and familiar. The equivalent explicit form is page.locator('id=save-button'). Choose one style and keep it consistent within a project.
What “get an element” means in Playwright
Playwright is designed around Locator objects rather than one-time DOM lookups. A locator describes how to find an element and resolves it when an action or assertion runs. This lets Playwright wait for an element to exist, become visible, become enabled, and be ready for the requested action.
const searchInput = page.locator('#search');
await searchInput.fill('playwright');
await expect(searchInput).toHaveValue('playwright');
Keeping the locator is preferable to extracting a stale element handle before the page finishes rendering.
CSS ID versus the explicit id= engine
| Selector | Example | When it helps |
|---|---|---|
| CSS ID | page.locator('#save-button') |
Short, widely understood, and ideal for a stable unique ID. |
| Playwright ID engine | page.locator('id=save-button') |
Makes the selector engine explicit when a locator string contains several selector styles. |
Both target the HTML id value. An ID should be unique in valid HTML. If the page contains duplicates, Playwright may resolve more than one element and an action that requires a single target can fail.
Prefer semantic locators when they describe the behavior
An ID is a good contract when it is stable and intentionally exposed for automation. When the user-facing meaning is more important, Playwright recommends role, label, text, or an explicit test ID. The locator guide explains that selectors tied closely to DOM structure can break during refactors.
// Use the accessible role when it is the real contract.
await page.getByRole('button', { name: 'Save' }).click();
// Use the associated label for a form control.
await page.getByLabel('Email').fill('user@example.com');
// Use a deliberate test contract.
await page.getByTestId('save').click();
Use these only when the corresponding role, accessible name, label, visible text, or test ID actually exists. Do not turn id="save-button" into getByTestId('save-button'): by default, getByTestId() searches for data-testid="save-button", not an HTML id. The default test ID attribute can be configured, for example to data-pw.
Complete JavaScript and TypeScript setup
Install and run
npm init playwright@latest
npx playwright test
JavaScript test
const { test, expect } = require('@playwright/test');
test('finds an element by id', async ({ page }) => {
await page.goto('https://example.com');
const heading = page.locator('#page-heading');
await expect(heading).toBeVisible();
await expect(heading).toContainText('Example');
});
TypeScript test
import { test, expect } from '@playwright/test';
test('updates a field selected by id', async ({ page }) => {
await page.goto('https://example.com/profile');
const name = page.locator('#display-name');
await name.fill('Ada Lovelace');
await expect(name).toHaveValue('Ada Lovelace');
});
Python Playwright equivalent
Python uses the same CSS ID syntax:
from playwright.sync_api import sync_playwright, expect
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto('https://example.com')
save_button = page.locator('#save-button')
expect(save_button).to_be_visible()
save_button.click()
browser.close()
The asynchronous API is equivalent:
import asyncio
from playwright.async_api import async_playwright, expect
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto('https://example.com')
button = page.locator('id=save-button')
await expect(button).to_be_enabled()
await button.click()
await browser.close()
asyncio.run(main())
Useful actions and assertions
const email = page.locator('#email');
await email.fill('user@example.com');
await expect(email).toHaveValue('user@example.com');
const status = page.locator('#status');
await expect(status).toHaveText('Saved');
await expect(status).toBeVisible();
const checkbox = page.locator('#terms');
await checkbox.check();
await expect(checkbox).toBeChecked();
Use assertions to describe the state you require before or after an action. Avoid adding arbitrary sleeps when an assertion or action can wait for the condition directly.
IDs in frames and iframes
A locator on the main page cannot see inside an iframe. First select the frame, then locate the ID within it:
const paymentFrame = page.frameLocator('iframe[title="Payment"]');
await paymentFrame.locator('#card-number').fill('4242424242424242');
If the frame is same-origin and you need a Frame object, you can obtain it after the frame appears:
const frame = page.frames().find(f => f.url().includes('/payment'));
if (!frame) throw new Error('Payment frame was not found');
await frame.locator('#card-number').fill('4242424242424242');
IDs inside shadow DOM
Playwright locators generally pierce open shadow DOM automatically. A selector such as #menu can therefore find an element in an open shadow root. Closed shadow roots cannot be inspected through normal page selectors; expose a test contract or interact through the component’s public UI instead.
Dynamic, duplicated, and unusual IDs
Dynamic IDs
If an ID contains a generated suffix, avoid hard-coding the unstable portion. Use a stable attribute or a CSS prefix/suffix selector only when that pattern is intentional:
const row = page.locator('[id^="order-"]');
await expect(row.first()).toBeVisible();
A semantic role, label, or test ID is usually clearer than depending on an implementation-generated value.
Duplicate IDs
Check the count when diagnosing a strictness error:
const matches = page.locator('#save-button');
console.log(await matches.count());
await expect(matches).toHaveCount(1);
If duplicates are intentional, narrow the locator to the correct region:
const dialog = page.getByRole('dialog');
await dialog.locator('#save-button').click();
Special characters
CSS IDs containing punctuation may need CSS escaping. Prefer a test ID or role contract when you control the markup. For IDs that begin with a digit or contain characters with CSS meaning, use Playwright’s explicit engine:
await page.locator('id=123-start').click();
Debugging and troubleshooting
| Error or symptom | Cause | Fix |
|---|---|---|
| “Locator resolved to more than one element” | The page has duplicate IDs or the selector is broad. | Fix the markup, assert a count of one, or scope to a dialog, row, or other container. |
Timeout waiting for #save-button |
The element is not on the current page, is inside a frame, is rendered later, or the ID differs. | Verify the URL and markup, use frameLocator() for iframes, and inspect the trace or screenshot. |
| Element is covered or not actionable | A modal, consent banner, animation, or overlay blocks it. | Wait for the blocking state to end, close the overlay through its own locator, or assert visibility and enabled state before clicking. |
getByTestId() finds nothing |
The markup has id=, not the configured test ID attribute. |
Use locator('#value') or add the project’s deliberate data-testid contract. |
| Selector works locally but not in CI | Timing, viewport, authentication, or responsive markup differs. | Use web-first assertions, set the required context explicitly, and inspect a trace from the failing retry. |
Inspect the selector interactively
await page.pause();
Run the test in headed mode and use Playwright Inspector to inspect the DOM and try locator suggestions:
npx playwright test --debug
You can also log the number of matches and the resolved HTML:
const target = page.locator('#save-button');
console.log('matches:', await target.count());
console.log('html:', await target.first().evaluate(el => el.outerHTML));
Performance and reliability guidance
- Keep a locator and reuse it instead of repeatedly querying the page with manual DOM evaluation.
- Prefer a unique, stable contract. Shorter selectors are easier to review and usually less sensitive to layout changes.
- Use role, label, or test ID when the ID is generated by a framework and can change between builds.
- Let Playwright auto-wait; replace fixed delays with assertions tied to the state your test needs.
- Scope selectors to a component or dialog when pages contain repeated controls.
- For parallel tests, isolate browser contexts and test data so a correct locator does not race another test’s state.
Or skip the browser setup
If your goal is a clean screenshot rather than an interactive test, ScreenshotNeo provides a single screenshot API request. Its capture can remove cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. The MCP server also lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf.
See the ScreenshotNeo API documentation for all options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const image = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', image);
ScreenshotNeo includes full-page capture with lazy images loaded, element capture by CSS selector, device presets, custom CSS and JavaScript, waits, headers, cookies, blocking rules, caching, signed links, asynchronous jobs, bulk capture, PDF output, and HTML/CSS rendering. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
What is the Playwright equivalent of document.getElementById()?
Use page.locator('#my-id') or page.locator('id=my-id'). Keep the Locator for later actions and assertions.
Should I use #id or id=value?
They select the same HTML ID. Use the CSS form for concise selectors and the explicit engine when making the selector strategy obvious.
Can I use getByTestId() with an HTML ID?
Not by default. getByTestId() targets data-testid (or your configured test ID attribute). Use locator('#id') for an HTML ID.
Why does my ID locator time out?
Confirm the page and frame, check the exact attribute value, and inspect a trace. The element may be rendered conditionally, hidden behind an overlay, or assigned a different ID in that environment.


