How to Find HTML Elements Missing Specific Attributes
Use CSS attribute selectors with :not() to find elements missing an attribute, then inspect every match with JavaScript.

Use the CSS selector tag:not([attribute]) to match HTML elements that do not have a particular attribute. For example, img:not([alt]) selects images with no alt attribute, and document.querySelectorAll('img:not([alt])') returns all such images in the document.
For a general markup audit, replace tag with the element name and attribute with the attribute to check: a:not([href]) finds links without href, while input:not([name]) finds inputs without name. If you want elements whose attribute exists but has an empty value, use a value selector instead, such as img[alt=""].
1. The selector pattern
CSS attribute selectors match elements according to whether an attribute is present or has a specified value. The negation pseudo-class :not() flips that test. Put together, :not([attribute]) means “does not have this attribute.”

/* Element type, followed by a negated attribute-presence test */
tag:not([attribute])
/* Common audit selectors */
img:not([alt])
a:not([href])
input:not([name])
button:not([type])
The selector can be used in a stylesheet as well as in JavaScript. A stylesheet can visually flag matches while you inspect a page; JavaScript can collect, report, or act on the matching elements.
2. Find all matches with JavaScript
Use document.querySelectorAll() when you need every match. It returns a static NodeList in document order. If there are no matches, the result is an empty list, so a loop simply runs zero times.
const missingAlt = document.querySelectorAll('img:not([alt])');
console.log(`Found ${missingAlt.length} image(s) without an alt attribute.`);
missingAlt.forEach((image) => {
console.log(image);
});
This is runnable in a browser page or DevTools console. The selector itself does not change the page. It only returns references to elements that match at the time the query runs.
To get just the first match, call document.querySelector(). It returns the matching element or null if nothing matches. Use the plural method when auditing a page, because a single result can hide additional problems.
const firstMissingAlt = document.querySelector('img:not([alt])');
if (firstMissingAlt) {
console.log('First image without alt:', firstMissingAlt);
} else {
console.log('Every image has an alt attribute.');
}
3. Check different elements and attributes
Use the same pattern for any element and attribute pair relevant to your markup. These examples find absent attributes, not attributes with empty values.
| Selector | What it matches |
|---|---|
img:not([alt]) |
Images without an alt attribute |
a:not([href]) |
Anchor elements without an href attribute |
input:not([name]) |
Inputs without a name attribute |
button:not([type]) |
Buttons without a type attribute |
[data-state]:not([aria-label]) |
Elements with data-state but without aria-label |
A selector can also combine an element with a class or other conditions. For example, img.product:not([alt]) checks only images with class product. Keep the selector aligned with the audit question: a broad selector may find legitimate cases as well as omissions.
Finding an absent attribute does not by itself tell you what value to add. The right content depends on the element’s purpose and the application context. Treat the query as a way to locate markup for review, not an automatic judgment about what every element should contain.
4. Distinguish missing from empty
img:not([alt]) matches only images where the alt attribute is absent. It does not match <img alt="">, because that element has an alt attribute whose value happens to be empty.

/* No alt attribute at all */
img:not([alt])
/* An alt attribute explicitly set to an empty string */
img[alt=""]
These are different markup states. Choose the selector based on what you mean by “missing.” If you are reviewing both states, query each separately and report them separately so a developer can decide what the markup should express.
const withoutAlt = document.querySelectorAll('img:not([alt])');
const emptyAlt = document.querySelectorAll('img[alt=""]');
console.log({
withoutAlt: withoutAlt.length,
emptyAlt: emptyAlt.length,
});
5. Scope the search to a container
Calling querySelectorAll() on an element searches its descendants. To make the root of a descendant query explicit in the selector, use :scope.
const container = document.querySelector('#content');
if (!container) {
throw new Error('Could not find #content');
}
const missingAlt = container.querySelectorAll(':scope img:not([alt])');
console.log(`Found ${missingAlt.length} image(s) in #content without alt.`);
This is useful when a page has separate regions and you want to inspect one of them. The :scope prefix anchors the selector to the query root; the remaining selector finds matching descendant images. You can omit it for a straightforward descendant query such as container.querySelectorAll('img:not([alt])').
Remember that a scoped query does not search outside the container. If the container selector itself returns null, handle that before calling querySelectorAll(), as in the example.
6. Make a reusable audit helper
For a quick browser-console report, define a helper that accepts a selector and returns a readable list. The selector is a CSS selector string, so pass a valid selector such as img:not([alt]).
function reportMatches(selector, root = document) {
const matches = root.querySelectorAll(selector);
console.log(`${selector}: ${matches.length} match(es)`);
matches.forEach((element, index) => {
console.log(index + 1, element);
});
return matches;
}
reportMatches('img:not([alt])');
reportMatches('a:not([href])');
The returned value remains a static NodeList. If the page later inserts or removes elements, run the query again to get an updated result. For a one-time audit, rerunning after the page has finished rendering is usually simpler than keeping a live observer.
7. Common mistakes and troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No matches, though an element looks incomplete | The attribute exists with an empty value, or the selector checks a different attribute. | Inspect the element in DevTools. Use a value selector such as img[alt=""] for an explicitly empty value. |
| Only one result appears | querySelector() returns the first match. |
Use querySelectorAll() to collect every match. |
| A scoped search misses elements elsewhere | The query runs on a container and searches its descendants only. | Use document.querySelectorAll() for the whole document, or select the correct container. |
A SyntaxError DOMException is thrown |
The selector string is invalid CSS, often due to a typo or malformed dynamic value. | Start with a literal selector and check brackets, quotes, and punctuation. Validate any selector built from input. |
| A later DOM change is not reflected in results | querySelectorAll() returns a static NodeList. |
Run the query again after the change or after the relevant content renders. |
If a value such as an ID or class name is inserted into a selector dynamically, it may contain characters that are not valid in that selector position. Escape dynamic identifiers with CSS.escape() where appropriate, or avoid constructing a selector and filter elements another way. Do not concatenate arbitrary text into CSS and assume it will parse.
const rawId = 'section:one';
const safeId = CSS.escape(rawId);
const section = document.querySelector(`#${safeId}`);
In HTML, attribute names are case-insensitive. Do not generalize that rule to attribute values: value matching depends on the attribute and language rules. For a missing-attribute test, prefer the literal attribute spelling used by the markup and verify unexpected results by inspecting the element.
8. Use a stylesheet to highlight omissions
If you want a visual review rather than a console list, temporarily add a CSS rule in DevTools or to a local development stylesheet. The rule highlights matching elements but does not repair them.
img:not([alt]) {
outline: 3px solid #d22;
outline-offset: 2px;
}
a:not([href]),
input:not([name]) {
outline: 2px dashed #b60;
}
Remove the temporary rule after the review. Keep the audit logic separate from production styling unless the visual treatment is part of the intended interface.
9. Performance and reliability notes
For normal page audits, a query like document.querySelectorAll('img:not([alt])') is a direct DOM lookup and is generally the simplest option. Prefer a specific element selector over a broad universal selector such as *:not([alt]), which can produce a large and noisy result set when nearly every element lacks that attribute.
Run the query after the markup you care about exists. A query made before client-rendered content is inserted cannot find future elements; the result is static and does not update itself. If an application renders asynchronously, run the audit after its render step or repeat it when you know the relevant content has loaded.
For reliable reports, record the selector and scope used, distinguish absent values from empty strings, and inspect representative matches before changing markup in bulk. A selector identifies structure; it cannot infer intent or decide the correct value.
10. Or skip the browser setup
If your audit includes capturing the rendered page for a report or review, ScreenshotNeo can return a screenshot with one API request. The browser-based approach above is still useful for querying and inspecting the DOM. ScreenshotNeo handles the capture step and supports screenshots or PDFs through its API.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Replace YOUR_API_KEY with your key and change the target URL as needed. ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status.
It also provides an MCP server for AI agents, including Claude, Cursor, and other MCP clients, with take_screenshot, get_page_info, and capture_pdf tools. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. See ScreenshotNeo for the service and the API documentation for request options.
Sign up for 1,000 free screenshots a month, with no card required.
11. Frequently asked questions
Can I find elements missing any attribute, whatever its name?
Yes. Use the element name followed by :not([attribute]), substituting the attribute you want to check. The attribute name must be valid in the selector.
Does this selector change or fix the HTML?
No. It finds matching elements. Updating markup is a separate decision and action.
Can I use it for elements created after the page loads?
Yes, by running the query after those elements exist. A previous NodeList remains static and will not gain new elements automatically.
What should I use to find a present but blank attribute?
Use an attribute-value selector, for example img[alt=""]. The missing-attribute selector intentionally excludes it.


