To add an element to an existing page, run DOM code in the browser context. With Puppeteer, call page.evaluate(), create the node with document.createElement(), fill it with textContent or deliberate HTML, and append it to the required parent. Carlo uses the same browser DOM operation inside the page script, although its official repository says Carlo is no longer maintained.
Add an element with Puppeteer
page.evaluate() evaluates a function in the page context, so the callback can use browser globals such as document. The callback is not ordinary Node.js code; DOM objects exist only inside that function.
Complete runnable example
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.setContent(`
<!doctype html>
<html>
<body>
<main id="content">
<h1>Account status</h1>
</main>
</body>
</html>
`);
await page.evaluate(() => {
const notice = document.createElement('p');
notice.textContent = 'Added by Puppeteer';
notice.className = 'automation-notice';
document.querySelector('#content').appendChild(notice);
});
console.log(await page.$eval('.automation-notice', el => el.textContent));
await browser.close();
})();
Install Puppeteer in a Node project, save the example as a JavaScript file, and run it with Node. The final log should be Added by Puppeteer. In a real workflow, replace page.setContent() with page.goto('https://example.com') when you need to modify a loaded site.
Insert into a particular parent
Choose the parent inside the page function, then append or insert the new node where it belongs:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
#1 Best Overall
await page.evaluate(() => {
const parent = document.querySelector('ul.results');
if (!parent) throw new Error('ul.results was not found');
const item = document.createElement('li');
item.textContent = 'Generated result';
parent.append(item);
});
append() places the node after the parent’s existing children. Use prepend() for the beginning, before() or after() relative to a reference node, or insertBefore() when you need precise control over an existing child.
Pass values from Node.js safely
Arguments can be passed to the page function explicitly. This keeps data separate from executable source text and avoids interpolating a value into JavaScript code.
const label = 'Build completed: 42 files';
await page.evaluate((value) => {
const status = document.createElement('p');
status.textContent = value;
document.body.appendChild(status);
}, label);
Use textContent for plain text. It inserts characters as text rather than parsing them as tags. If the requirement is intentionally to create markup, use an explicit DOM construction strategy and treat untrusted input as data, not HTML.
Create attributes, styles and children
await page.evaluate(() => {
const card = document.createElement('article');
card.setAttribute('data-source', 'automation');
card.classList.add('card');
const heading = document.createElement('h2');
heading.textContent = 'New card';
const body = document.createElement('p');
body.textContent = 'This content was added without replacing the page.';
card.append(heading, body);
document.body.appendChild(card);
});
For a one-off visual change, set a style property or add a class whose stylesheet already exists. For reusable CSS, add a stylesheet separately; Puppeteer’s addStyleTag() adds a style or link tag, not an arbitrary content element.
Use existing elements instead of creating duplicates
Creation and lookup are different operations. If the element already exists, select it and interact with it; do not create a second copy.
Rank #2
const text = await page.$eval('#account-status', element => element.textContent);
console.log(text);
$eval(selector, fn) passes the first matching element to the callback and throws when no element matches. For actions that depend on an element appearing or becoming actionable, Puppeteer’s locator APIs are preferable because they wait for the required state. A selector lookup never “adds” an element; only DOM creation followed by insertion does that.
Wait for a dynamic parent
await page.waitForSelector('#content');
await page.evaluate(() => {
const parent = document.querySelector('#content');
const note = document.createElement('p');
note.textContent = 'Loaded after the parent appeared';
parent.appendChild(note);
});
When the parent is rendered by a framework, wait for a stable selector or use a locator before evaluating the DOM operation. If the page replaces that parent later, your inserted node can disappear; insert after the application’s render completes or use the application’s own state/update mechanism.
setContent() versus appending one node
page.setContent(html) assigns the page’s markup. It is useful for a test fixture, email preview or a page you intend to define from scratch. It is not the focused operation when an existing document must remain intact and receive one additional element.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →await page.setContent('<main><h1>Fixture</h1></main>');
// This replaces the document content supplied by the page.
await page.evaluate(() => {
const badge = document.createElement('span');
badge.textContent = ' appended';
document.querySelector('h1').append(badge);
});
Use goto() followed by evaluate() for a live page you want to preserve. Use setContent() when replacing the document is the actual test or rendering goal.
How Carlo adds an element
Carlo’s README demonstrates the same browser-side pattern: create a node, assign its text, and append it to document.body. A simplified page-side example is:
const div = document.createElement('div');
div.textContent = `${type}: ${data[type]}`;
document.body.appendChild(div);
Carlo is a headful Node application framework that uses locally installed Chrome and the Puppeteer project. Its repository README explicitly states, “Carlo is no longer maintained.” That status matters for a new project: existing Carlo applications can still use the standard DOM technique, but a maintained browser-automation stack is generally a safer starting point for new work.
Keep the Node/page boundary narrow
Carlo can expose a Node function to page code, allowing the page to request data or a capability. Expose only the specific operation required and validate values crossing the boundary. The README’s example exposes environment data to illustrate the mechanism; it should not be treated as a recommendation to expose an entire process environment to a page.
HTML text, markup and security choices
Plain text
Use textContent when the value is a label, status, user name or other text:
await page.evaluate((username) => {
const greeting = document.createElement('p');
greeting.textContent = `Hello, ${username}`;
document.body.append(greeting);
}, username);
Intentional markup
If you intentionally need nested markup, build each node with DOM methods, or assign innerHTML only to a string whose source and contents you control. Never treat untrusted input as HTML merely because it contains angle brackets. Building child nodes separately makes the intended structure and text handling explicit.
Troubleshooting
document is not defined
The code ran in Node instead of the page. Put DOM statements inside the callback passed to page.evaluate(), or inside Carlo’s page script. Node-side code can prepare values, then pass them as arguments.
Rank #4
“Cannot read properties of null”
Your parent selector matched nothing at the time of insertion. Check the selector spelling, navigate to the intended page, and wait for the parent with waitForSelector() or a locator before evaluating.
$eval throws an error
$eval() requires at least one matching element. Confirm that the page has loaded the relevant route and that the selector is not scoped to a different frame. If absence is expected, use a nullable lookup pattern and branch instead of calling $eval() unconditionally.
The element appears briefly, then vanishes
A client-side render replaced the parent or rerendered its children. Insert after the render completes, target a stable container, or update the framework’s state rather than mutating a managed subtree.
Text displays as tags
That is expected when using textContent; it treats markup-looking characters as text. If the requirement is actual nested elements, create those child nodes explicitly and append them.
The script hangs while loading
Navigation and page scripts can keep running because of long-lived requests. Wait for a specific selector or application-ready condition rather than relying on a broad network-idle condition, and set an appropriate navigation or operation timeout.
Free tools Windows power users keep installed
One-click scans. No signup required.
Best Value
- Used Book in Good Condition
The change is not visible in a screenshot
Capture only after the DOM mutation completes. If fonts, images or a framework update affect layout, wait for the relevant element and, where necessary, the page’s visual assets before taking the screenshot.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Performance and reliability practices
- Perform related DOM changes in one
evaluate()call to reduce Node-to-page round trips. - Use stable IDs or data attributes instead of fragile positional selectors.
- Make insertion idempotent when a script might retry: first check for a marker such as
[data-automation-notice]. - Return a small serializable result, such as a boolean or text value, when the Node process needs confirmation.
- Keep browser-only objects inside the page callback; return plain data rather than DOM nodes.
- For iframes, obtain the correct frame and evaluate in that frame’s document; the top-level page document cannot directly select nodes inside a separate frame.
Idempotent insertion example
const inserted = await page.evaluate(() => {
if (document.querySelector('[data-added-by=automation]')) return false;
const note = document.createElement('p');
note.dataset.addedBy = 'automation';
note.textContent = 'Inserted once';
document.body.appendChild(note);
return true;
});
console.log(inserted ? 'Inserted' : 'Already present');
Or skip the browser setup
If your goal is a clean image or PDF rather than controlling a browser yourself, ScreenshotNeo provides a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each cleanup step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits cost nothing, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers.
One GET request is enough:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the complete parameter list and response details in the ScreenshotNeo documentation.
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', data);
ScreenshotNeo also offers full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets plus custom viewports, retina scale, PDF paper and page-range controls, custom CSS and JavaScript, click-before-capture actions, selector hiding, selector/delay/network-idle waits, request and resource blocking, custom headers/cookies/user agents and Authorization, timezone and geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API and an OpenAPI specification. Parameter names used by other screenshot APIs also work.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchAn MCP server supplies take_screenshot, get_page_info and capture_pdf tools to Claude, Cursor and other MCP clients, so an AI agent can perform the capture without custom Puppeteer code. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Choosing between the approaches
| Need | Use Puppeteer | Use Carlo | Use ScreenshotNeo |
|---|---|---|---|
| Add or modify live DOM nodes in an automation flow | Yes; use page.evaluate() |
Possible in existing apps | Not its purpose |
| Start a new browser application framework | Maintained Puppeteer documentation and APIs are the practical choice | Repository says no longer maintained | Not a browser framework |
| Produce a cleaned screenshot or PDF | Requires browser setup and your own cleanup logic | Requires browser setup | One API request, with cleanup and billing verdicts |
| Let an AI agent capture pages | Requires integrating your own agent flow | Requires integrating your own agent flow | MCP tools are included |
Frequently Asked Questions
Can I add an element without opening a visible browser window?
Yes. Puppeteer can run headless; visibility of the browser window does not change where the DOM code executes.
Does adding a node change the website for other visitors?
No. The mutation affects the page instance controlled by your browser. It is not a server-side edit unless your code separately sends data to the site’s backend.
Can the same technique work inside an iframe?
Yes, but evaluate against the iframe’s frame object rather than the top-level page, because each frame has its own document.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

