How to Select Elements by ID Using CSS Selectors
Learn ID selectors in CSS and JavaScript, escape unusual IDs, handle duplicates, and capture selected elements reliably.

Use a hash followed by the exact value of the element’s id attribute. For example, #demo selects <div id="demo"> in CSS. In JavaScript, use document.querySelector('#demo') when you need a CSS selector, or document.getElementById('demo') when you already have an ID value.
The important details are exact matching, case sensitivity, escaping values that are not valid CSS identifiers, and keeping IDs unique. This guide covers styling, JavaScript retrieval, dynamic IDs, duplicate IDs, troubleshooting, and a practical way to capture a selected element without maintaining a browser automation stack.
1. The direct answer: use #id
HTML assigns the identifier:
<button id="save-button">Save</button>
CSS selects it with a hash:
#save-button {
background: #0b63ce;
color: white;
padding: 0.75rem 1rem;
border: 0;
border-radius: 0.4rem;
}
The CSS ID selector matches an element based on the value of its id attribute. The value must match exactly. A selector such as #Save-button does not match id="save-button", because IDs are case-sensitive.
You can also combine an ID with a type selector:
button#save-button {
font-weight: 700;
}
p#introduction {
max-width: 65ch;
}
A type selector comes before the ID selector in a compound selector. Use the extra type constraint only when it expresses a real requirement; #save-button is usually clearer and less brittle.
2. Selecting an ID in JavaScript
querySelector()
document.querySelector() accepts any valid CSS selector string and returns the first matching element, or null when there is no match.

const saveButton = document.querySelector('#save-button');
if (saveButton) {
saveButton.addEventListener('click', () => {
console.log('Saved');
});
}
Because it accepts CSS, you can extend the selector when necessary:
const enabledSaveButton = document.querySelector(
'button#save-button:not([disabled])'
);
An invalid selector causes querySelector() to throw a SyntaxError. This is why dynamic IDs must be escaped before interpolation.
getElementById()
document.getElementById() is the direct ID-specific method. Pass the ID value without the hash:
const saveButton = document.getElementById('save-button');
For a normal ID, this is equivalent in purpose to document.querySelector('#save-button'). The APIs differ in what they accept: getElementById() takes only an ID value, while querySelector() takes a complete CSS selector.
querySelectorAll()
querySelectorAll() returns all elements matching a selector. IDs are intended to be unique, but this method is useful for diagnosing invalid markup with duplicate IDs:
const matches = document.querySelectorAll('#save-button');
console.log(matches.length);
By contrast, querySelector('#save-button') returns only the first match in depth-first document order.
3. CSS selector versus JavaScript method
| Need | Use | Input | Result |
|---|---|---|---|
| Style an element | #id in CSS |
CSS selector | All matching elements are styled |
| Retrieve with a CSS condition | querySelector() |
Any valid CSS selector | First element or null |
| Retrieve by ID only | getElementById() |
ID value, without # |
The matching element or null |
| Inspect every match | querySelectorAll() |
Any valid CSS selector | A collection of all matches |
Choose getElementById() when the ID is already a plain, trusted string and no additional selector logic is needed. Choose querySelector() when you need to combine the ID with a type, state, attribute, or descendant selector.
4. Escape IDs that contain punctuation or start with a number
HTML permits ID values that are not valid CSS identifiers. For example:
<div id="item:42">Details</div>
This is not safe to interpolate directly into a CSS selector:
// Can throw SyntaxError because ':' has selector meaning.
const item = document.querySelector('#item:42');
Use CSS.escape() for dynamic or unusual values:
const id = 'item:42';
const item = document.querySelector(`#${CSS.escape(id)}`);
if (item) {
item.hidden = false;
}
A literal CSS rule needs an escaped character. For example, an ID containing a question mark can be written as #item\\?one. An ID beginning with digits can be escaped as #\\00003123item. In JavaScript string literals, remember that the backslash itself must be escaped when needed.
const numericId = '123item';
const element = document.querySelector(`#${CSS.escape(numericId)}`);
When you control the markup, prefer simple IDs made from letters, digits, hyphens, and underscores. When you do not control it, always escape the value before building a selector.
5. Exact matching, case, and duplicate IDs
Exact values
#profile matches id="profile", not id="profile " with a trailing space and not id="Profile". Check the rendered DOM in browser developer tools rather than relying on a template variable that may have been transformed.
IDs should be unique
An ID is intended to identify one element in a document. Duplicate IDs make CSS and JavaScript behavior ambiguous. CSS can match every element carrying the same value, while querySelector() returns the first match in document order.
<div id="card">First</div>
<div id="card">Second</div>
document.querySelectorAll('#card').length; // 2
document.querySelector('#card').textContent; // "First"
Fix the markup by assigning unique IDs or use a class when multiple elements intentionally share a style or behavior.
6. Practical patterns
Toggle an element by ID
const panel = document.getElementById('settings-panel');
const toggle = document.getElementById('settings-toggle');
toggle?.addEventListener('click', () => {
if (panel) panel.hidden = !panel.hidden;
});
Style a selected element with a class
const status = document.querySelector('#status');
status?.classList.add('is-ready');
Select an ID inside a component root
const dialog = document.querySelector('#account-dialog');
const closeButton = dialog?.querySelector('#close-dialog');
Use a scoped query when the component structure is known. If IDs are globally unique, querying the document directly is simpler.
Read and update content safely
const heading = document.getElementById('page-heading');
if (heading) {
heading.textContent = 'Updated heading';
}
textContent treats the value as text. Use innerHTML only when you intentionally need to insert trusted markup.
7. Capturing an element selected by ID
If your goal is a screenshot of one element, the same selector can be passed to a browser automation tool that supports element capture. A typical workflow is:
- Load the page and wait until the target element exists.
- Use
#idas the CSS selector. - Capture the element rather than the entire viewport.
- Escape the ID when it is dynamic or contains punctuation.
For a page containing <section id="pricing">, the selector is simply #pricing. If the ID is plan:pro, construct it with CSS.escape('plan:pro') before passing it to the capture call.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its element capture option accepts a CSS selector, so you can request the element selected by ID without installing or managing a browser.

See the ScreenshotNeo API documentation for the complete parameter list and response details.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
--data-urlencode selector='#pricing' \
-o pricing.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://stripe.com",
"selector": "#pricing",
},
timeout=90,
)
r.raise_for_status()
open("pricing.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
selector: '#pricing',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('pricing.webp', image));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. 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. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is available on every plan. Create a free ScreenshotNeo account.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No CSS styles apply | The selector does not exactly match the ID, or the stylesheet is not loaded. | Inspect the rendered id, check capitalization and whitespace, and verify the stylesheet request. |
querySelector() throws SyntaxError |
The ID contains punctuation or starts with a value that is invalid in a CSS identifier. | Use CSS.escape(id) before interpolation. |
JavaScript returns null |
The element is not in the current document yet, the ID is misspelled, or the element is inside another document. | Run the query after the markup exists and inspect the live DOM. For an iframe, query its document after it loads. |
| The wrong element is returned | Duplicate IDs exist. | Make IDs unique or use a class and a more specific selector. |
| An element screenshot is blank | The selector did not resolve before capture, or the page had not finished rendering. | Wait for the selector, add a short delay or network-idle wait, and confirm the selector in the browser. |
| Dynamic IDs work sometimes | The ID changes between renders or contains unescaped characters. | Build the selector from the current value and call CSS.escape(); prefer a stable data attribute when you control the page. |
10. Performance, reliability, and cost notes
For a single known ID, getElementById() communicates the intent directly. The practical performance difference from querySelector('#id') is usually less important than querying at the correct time and avoiding repeated work in hot event loops. Cache a reference when the same element is used repeatedly.
const status = document.getElementById('status');
for (const message of messages) {
if (status) status.textContent = message;
}
Reliability comes from stable identifiers, unique markup, escaping untrusted values, and waiting for the element’s lifecycle. In server-rendered or client-rendered pages, attach behavior after the relevant DOM has been created.
For automated screenshots, full-page captures may require lazy images to load and can take longer than an element capture. A selector wait is more deterministic than an arbitrary long delay. ScreenshotNeo also supports custom CSS and JavaScript, request blocking, headers, cookies, user agents, timezone, geolocation, caching with a chosen TTL, retries through asynchronous jobs, signed webhooks, and bulk capture of up to 100 URLs per call. Use only the options your page needs so requests remain easy to diagnose.
ScreenshotNeo charges only for clean shots. Cache hits and unsuccessful page outcomes such as bot checks, blank pages, timeouts, and failed loads cost nothing. The response headers let you record the verdict and billing result alongside your asset pipeline.
11. A short checklist
- Write the selector as
#exact-id. - Keep every ID unique and case-sensitive.
- Use
getElementById('exact-id')for direct lookup. - Use
querySelector('#exact-id')when combining CSS conditions. - Use
querySelectorAll()to detect duplicate matches. - Escape dynamic values with
CSS.escape(). - Wait until the target element exists before querying or capturing it.
12. FAQ
Should I include the hash in getElementById()?
No. Pass only the ID value: getElementById('demo'). The hash is part of the CSS selector syntax used by querySelector() and stylesheets.
Can an ID selector match more than one element?
It can if the document contains duplicate IDs. That markup violates the intended uniqueness of IDs. CSS may style all matches, while querySelector() returns the first.
Why does #123item fail?
A leading number can make the value invalid as an unescaped CSS identifier. Use CSS.escape('123item') when constructing the selector.
When should I use a class instead?
Use a class when several elements share the same style or behavior. Reserve IDs for unique elements that need a stable reference, fragment target, label association, or script hook.
Does querySelector() return a live collection?
No. It returns one element or null. Use querySelectorAll() when you need all current matches.