How to Extract HTML Attributes From Web Elements
Read href, src, class, data-* and other HTML attributes with browser JavaScript, Playwright or Selenium—and learn when you need a live DOM property instead.

To read an HTML attribute from an element already in the browser DOM, select the element and call getAttribute(). For example, document.querySelector('a')?.getAttribute('href') returns the link’s href value, or null when the element exists but has no such attribute. If no element matches, the optional chain returns undefined. For Playwright, use locator.getAttribute(); for Selenium Python, use get_dom_attribute() when you specifically want the markup attribute.
This guide covers single and multiple elements, missing values, attribute-versus-property behavior, dynamic pages, and common failures. These methods read a DOM that has already been loaded or created; they do not fetch and parse an arbitrary URL by themselves.
1. Read an attribute with browser JavaScript
Element.getAttribute(name) returns the named attribute’s string value, or null if the attribute is absent. In an HTML document, the name passed to the method is normalized to lowercase. HTML character references are decoded as the document is parsed, so the returned value reflects the parsed attribute value. See MDN’s getAttribute() reference.
const link = document.querySelector('a');
if (!link) {
console.log('No matching link');
} else {
const href = link.getAttribute('href');
if (href === null) {
console.log('The link has no href attribute');
} else {
console.log(href);
}
}
The two checks represent different situations: the selector may find no element, or it may find an element that lacks href. Optional chaining is concise, but its result can be either undefined (no element) or null (element present, attribute absent):
const href = document.querySelector('a')?.getAttribute('href');
if (href == null) {
console.log('No element or no href attribute');
} else {
console.log(href);
}
The loose null check == null intentionally matches both null and undefined. If you need to distinguish the cases, keep the explicit element check shown above.
Common attributes
Pass the attribute name as a string. The selector and the attribute name serve different purposes: the selector finds the element, while the argument to getAttribute() names the value to read.
const image = document.querySelector('img.product');
const src = image?.getAttribute('src');
const alt = image?.getAttribute('alt');
const className = image?.getAttribute('class');
const id = image?.getAttribute('id');
const label = image?.getAttribute('aria-label');
const productId = image?.getAttribute('data-product-id');
This works for standard attributes such as href, src, title, id, class, and aria-label, as well as custom data-* attributes. If the page uses a different selector or attribute name, substitute it directly.
2. Select the intended element first
Attribute extraction is only as precise as the locator. querySelector() returns the first match; make the selector specific enough to identify the intended node. For example, main article a.download is often more targeted than a. The browser’s querySelector() and querySelectorAll() use CSS selectors.

const downloadLink = document.querySelector('main article a.download');
const href = downloadLink?.getAttribute('href');
If you need to inspect which nodes matched, collect them before extracting:
const matches = document.querySelectorAll('main article a.download');
console.log(`Matched ${matches.length} elements`);
const hrefs = [...matches].map(element => element.getAttribute('href'));
console.log(hrefs);
querySelectorAll() returns a static list of the elements matched at the time of the call. Each mapped value can be null if that matching element has no href. To keep only links that have one:
const hrefs = [...document.querySelectorAll('main article a.download')]
.map(element => element.getAttribute('href'))
.filter(href => href !== null);
For a particular data-id attribute, the attribute selector can both locate matching elements and let you read their values:
const values = [...document.querySelectorAll('[data-id]')]
.map(element => element.getAttribute('data-id'));
console.log(values);
If you expect exactly one element, check the count rather than silently accepting the first match. Duplicate matches may mean the selector is too broad or the page has repeated components.
const matches = document.querySelectorAll('#account a.profile');
if (matches.length !== 1) {
throw new Error(`Expected one profile link; found ${matches.length}`);
}
const href = matches[0].getAttribute('href');
3. Attribute or DOM property? Choose based on the value you need
An HTML content attribute is the value written in the markup. A DOM property is a JavaScript value exposed by the element object and can represent current state. For example, an input’s value attribute describes its markup default, while the value property can reflect what a user has typed. If the requirement is “what is currently in the input?”, read the property:

const input = document.querySelector('input[name="email"]');
const markupValue = input?.getAttribute('value');
const currentValue = input?.value;
console.log({ markupValue, currentValue });
For a link, getAttribute('href') reads the content attribute, while link.href is a property that commonly exposes a resolved URL. If you need the literal markup value, use the attribute method; if you need the browser’s resolved URL, use the property.
const link = document.querySelector('a');
console.log('Attribute:', link?.getAttribute('href'));
console.log('Resolved property:', link?.href);
Do not substitute textContent, innerHTML, or outerHTML when the task is to get one attribute. Those expose text or markup, not the requested attribute value. For data attributes, JavaScript also exposes element.dataset, such as element.dataset.productId for data-product-id. Use getAttribute() when you want to work with the literal attribute name, or when you need to distinguish a missing attribute from an empty value.
4. Read attributes in Playwright
In Playwright, a locator identifies the element and getAttribute() reads its attribute. This example assumes that page has already been created and navigated to the relevant page:
const link = page.locator('main article a.download');
const href = await link.getAttribute('href');
if (href === null) {
console.log('The matching link has no href attribute');
} else {
console.log(href);
}
Locators are designed to resolve elements when an action or read occurs, which is useful for pages whose DOM changes during a test. If the selector matches multiple elements where one is expected, narrow it or choose the intended match explicitly. For an assertion, Playwright recommends the retry-aware toHaveAttribute() matcher instead of reading once and comparing, which can be flaky when the page is still updating. See the official Playwright Locator API.
import { test, expect } from '@playwright/test';
test('download link has the expected destination', async ({ page }) => {
await page.goto('https://example.com');
await expect(page.locator('main article a.download'))
.toHaveAttribute('href', '/downloads/guide.pdf');
});
The test is runnable within a Playwright Test project after installing and configuring Playwright. Replace the example URL, selector, and expected value with the page under test. Use getAttribute() when your code needs the value; use toHaveAttribute() when the purpose is to assert that the page eventually reaches a state.
5. Read markup attributes in Selenium Python
First locate the element, then call get_dom_attribute() for the markup attribute. Selenium Python returns None if the attribute is missing. The official WebElement API explains that get_attribute() checks the property first and falls back to the attribute; it can also coerce certain boolean-like values. Use get_dom_attribute() when you specifically need the DOM attribute. Selenium’s element finder guide documents the find-then-read workflow.
from selenium import webdriver
from selenium.webdriver.common.by import By
URL = 'https://example.com'
driver = webdriver.Chrome()
try:
driver.get(URL)
link = driver.find_element(By.CSS_SELECTOR, 'main article a.download')
href = link.get_dom_attribute('href')
if href is None:
print('The matching link has no href attribute')
else:
print(href)
finally:
driver.quit()
This assumes Selenium and a compatible browser driver are installed and available. For a normal Selenium workflow, the driver configuration depends on your local environment. If the element might not exist, find_element() raises a no-such-element exception before attribute reading; catch that exception only when absence is an expected outcome. If several matches are expected, use find_elements() and iterate:
links = driver.find_elements(By.CSS_SELECTOR, 'main article a.download')
for link in links:
print(link.get_dom_attribute('href'))
Use get_property('value') when you need the current value of a form control, rather than assuming its markup attribute is current. Selenium’s convenience get_attribute() is useful when property-first behavior is desired, but its name does not mean “raw markup attribute.”
6. Extract an attribute from a dynamic page
Client-rendered pages may not create the target element immediately after navigation. Reading too early can produce no matching element, a timeout, or a value from an earlier state. Wait for the actual condition you need. In Playwright, locators and assertions can wait for matching state; in Selenium, use an explicit wait for the element.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
URL = 'https://example.com'
selector = 'main article a.download'
driver = webdriver.Chrome()
try:
driver.get(URL)
link = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, selector))
)
print(link.get_dom_attribute('href'))
finally:
driver.quit()
Waiting for presence only confirms that the element exists. If the attribute itself is populated after the element appears, wait for the attribute’s value or repeatedly inspect it until the expected condition is true. Avoid arbitrary long sleeps as the default: they slow fast pages and can still be too short for slow ones.
For an element inside an iframe, switch into that frame before locating it. For content inside a shadow root, use the framework’s supported shadow DOM locators. A selector run against the top-level document does not automatically search those separate contexts.
7. Troubleshooting common errors
| Symptom | Likely cause | Fix |
|---|---|---|
Cannot read properties of null |
The selector found no element, then code called a method on the missing result. | Check the result of querySelector() before reading the attribute; verify the selector and page state. |
JavaScript returns null |
The element exists but does not have the named attribute. | Check the attribute name and element; handle null before string operations. |
| Selenium reports no such element | The locator matched nothing in the current document or frame. | Verify the selector, wait for rendering, and switch to the correct frame when needed. |
| Selenium returns an unexpected value | get_attribute() may return a property value before the attribute value. |
Use get_dom_attribute() for markup or get_property() for live state. |
| Wrong element’s value is returned | A broad selector matched several nodes and the first was used. | Scope the selector to a container, inspect match count, or iterate all matches. |
| Value appears stale or empty | The page has not finished rendering or the application updates the element later. | Wait for the specific element or expected attribute state; avoid relying on a fixed delay. |
| Attribute appears absent in a page fetch | The HTML response may not contain client-rendered content that appears after JavaScript runs. | Inspect the live DOM with a browser automation tool, or identify the underlying data source when appropriate. |
Also distinguish an empty attribute from an absent one. An element like <input aria-label=""> has the attribute; JavaScript returns the empty string "", not null. A truthiness check would treat both as false, so compare explicitly with null when presence matters.
8. Performance, reliability, and cost
For a single element, the attribute lookup itself is usually a small part of the work. On a large page or across many URLs, the expensive portion is commonly loading pages and running a browser, not calling getAttribute(). Narrow selectors, avoid repeatedly querying the same broad subtree, and collect the needed values in one pass. Keep browser sessions managed with cleanup such as Selenium’s finally block so failures do not leave processes running.
Reliability depends on targeting the right page state. Prefer stable selectors such as a meaningful data attribute or accessible role where available; avoid brittle positional selectors if the page structure changes frequently. For tests, use Playwright’s retrying assertions or Selenium explicit waits instead of immediate read-and-compare against a changing page. Treat missing attributes as normal input where the site permits them.
Browser automation has resource and infrastructure costs: browsers consume memory and CPU, and remote browser services may charge for sessions or usage. This guide does not assume a benchmark or a particular provider’s rates. If you only need a rendered screenshot rather than DOM attribute values, a screenshot API can avoid maintaining browser setup, but a screenshot is an image and cannot return the underlying HTML attribute as structured data.
Or skip the browser setup
If your task is to capture what a page looks like after it loads, ScreenshotNeo provides a website screenshot API and MCP server. It does not extract DOM attributes; use the browser methods above when you need the actual attribute value. For a visual capture, the one-call request is:
ScreenshotNeo API documentation
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 bytes = await res.arrayBuffer();
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(bytes)));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, with no card.
9. Frequently asked questions
Does getAttribute() return a number or boolean?
In browser JavaScript, it returns a string when the attribute is present and null when absent. Convert or interpret the string yourself if your application needs another type.
How do I get every attribute on an element?
Use the element’s attributes collection and iterate its entries. If you only need known values, reading those by name is simpler and makes the intended output clear.
const element = document.querySelector('.product');
const allAttributes = element
? Object.fromEntries([...element.attributes].map(attr => [attr.name, attr.value]))
: null;
Can I read attributes without loading the page in a browser?
Yes, if the HTML is already available as a string, parse it with an HTML parser and query the parsed document. A plain HTTP fetch may not include elements created later by page JavaScript, so it may not represent the rendered DOM.
Why do getAttribute('href') and element.href differ?
The attribute is the markup value; the property may expose a URL resolved against the document’s base URL. Choose the one that matches whether you need source markup or browser-interpreted state.
10. Quick reference
| Environment | Read an attribute | Missing value |
|---|---|---|
| Browser JavaScript | element.getAttribute('href') |
null |
| Playwright | await locator.getAttribute('href') |
null |
| Selenium Python markup attribute | element.get_dom_attribute('href') |
None |
| Selenium Python live property | element.get_property('value') |
Depends on the property and element |
The reliable pattern is consistent: locate the intended element, decide whether you need its content attribute or live property, read that value with the matching API, and handle the missing case explicitly.


