Use browser-side DOM code to add an element without replacing the page. In Puppeteer, call page.evaluate(), create a node with document.createElement(), fill it with textContent, and append it to the required parent. Carlo uses the same DOM operation in a script running inside the page. The operation is standard JavaScript; the difference is how each framework loads the page and crosses the Node.js/browser boundary.
Puppeteer: add an element to an existing page
page.evaluate() evaluates a function in the page context and returns its result. Because the callback runs in the browser, it can access document, window, and other DOM APIs. The following complete example opens a page, appends a paragraph to its body, and writes the result to an image.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.goto('https://example.com', {waitUntil: 'networkidle2'});
await page.evaluate(() => {
const notice = document.createElement('p');
notice.textContent = 'Added by Puppeteer';
document.body.appendChild(notice);
});
await page.screenshot({path: 'page.png', fullPage: true});
await browser.close();
})();
The callback is not ordinary Node.js code. Node variables are not automatically visible inside it. Pass values as arguments instead of interpolating them into source text:
const message = 'Build finished';
const selector = '#status';
await page.evaluate((text, parentSelector) => {
const parent = document.querySelector(parentSelector);
if (!parent) throw new Error(`Parent not found: ${parentSelector}`);
const item = document.createElement('p');
item.textContent = text;
parent.appendChild(item);
}, message, selector);
Passing arguments keeps the executable function fixed and makes the data boundary explicit. For plain text, textContent is the safe, unambiguous choice: characters such as < are inserted as text rather than interpreted as tags.
Recommended Free Tools
#1 Best Overall
Insert at a precise location
Choose the parent in the page, then use appendChild, prepend, before, or after according to the required position.
await page.evaluate(() => {
const article = document.querySelector('article');
if (!article) throw new Error('article element was not found');
const heading = document.createElement('h2');
heading.textContent = 'Generated section';
article.prepend(heading);
const note = document.createElement('p');
note.textContent = 'This paragraph follows the article.';
article.appendChild(note);
});
If you intentionally need markup, assign it explicitly with innerHTML or construct each child node separately. Never treat untrusted input as trusted HTML. Building nodes and assigning text separately avoids accidental markup interpretation.
Add attributes, classes, and styles
await page.evaluate(() => {
const badge = document.createElement('span');
badge.className = 'status-badge';
badge.setAttribute('role', 'status');
badge.textContent = 'Ready';
badge.style.color = 'green';
document.body.appendChild(badge);
});
For substantial styling, add a class and inject a stylesheet, or use Puppeteer’s addStyleTag(). That method adds a stylesheet tag; it is not a generic replacement for creating content elements. Likewise, addScriptTag() adds a script tag rather than a DOM node of your choice.
Creating versus selecting an element
Adding means creating a new node and inserting it. If the node already exists, select it and change its contents instead.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesawait page.$eval('#status', element => {
element.textContent = 'Updated';
});
$eval(selector, fn) passes the first matching element to the function and throws when there is no match. For newer Puppeteer code, locators are preferable for interactions because they wait for an element to be present and in a suitable state. A selector lookup does not create anything; use document.createElement() when a new node is required.
Rank #2
When to use page.setContent()
page.setContent(html) assigns HTML as the page’s content. It is useful for a new fixture, email preview, or isolated test document. It is not the focused operation when an existing page should remain intact and receive one additional element.
await page.setContent(`
<!doctype html>
<html><body>
<main id="fixture"></main>
</body></html>
`);
Use setContent() when you own the whole document; use evaluate() with DOM methods when you are augmenting a loaded page.
Carlo: perform the same DOM operation in the page
Carlo is a headful Node application framework that uses a locally installed Chrome and the Puppeteer project. Its README demonstrates creating a div, assigning textContent, and appending it to document.body inside the page script:
Free tools Windows power users keep installed
One-click scans. No signup required.
const div = document.createElement('div');
div.textContent = `${type}: ${data[type]}`;
document.body.appendChild(div);
The DOM code is identical in principle to the Puppeteer example. Carlo can expose a Node function to the page when the page needs application data or a capability. Keep that bridge narrow: expose only the specific operation and values required by the page. The README’s broad environment-data example is illustrative, not a recommendation for exposing process secrets.
Carlo’s repository README states, “Carlo is no longer maintained.” That makes it a poor default for a new project unless you have a specific legacy application to support. Existing Carlo code can still use standard DOM APIs, but plan migration and avoid building new dependencies around an unmaintained framework.
Choosing Puppeteer or Carlo for this task
| Question | Puppeteer | Carlo |
|---|---|---|
| Where does DOM code run? | In the callback passed to page.evaluate(), inside the browser page. |
In the page script, using the same browser DOM APIs. |
| How is a new node created? | document.createElement(), populate it, then append or insert it. |
The same three-step DOM operation. |
| Node-to-page communication | Pass serializable arguments to evaluate(). |
Expose narrowly scoped Node functions or data to the page. |
| Project status | Active Puppeteer documentation search results displayed version 25.12.0 on September 30, 2026; your installed version may differ. | The official README says Carlo is no longer maintained. |
| Best fit | Automated browser control, testing, scraping, and rendering. | Maintaining an existing Carlo desktop-style app. |
There is no established performance or compatibility result that justifies claiming one framework is faster for this DOM operation. The browser performs the same fundamental work.
Reliable insertion patterns
Wait for the parent before inserting
await page.waitForSelector('#results');
await page.evaluate(() => {
const results = document.querySelector('#results');
const p = document.createElement('p');
p.textContent = 'Loaded';
results.appendChild(p);
});
If the parent is rendered by a client-side application, waiting for navigation alone may be insufficient. Wait for a selector, a known state, or application-specific readiness signal.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Make repeated runs idempotent
await page.evaluate(() => {
const old = document.querySelector('[data-automation="notice"]');
if (old) old.remove();
const notice = document.createElement('p');
notice.dataset.automation = 'notice';
notice.textContent = 'Added once';
document.body.appendChild(notice);
});
A marker attribute lets retries replace the prior node instead of producing duplicates.
Return a small serializable result
const result = await page.evaluate(() => {
const node = document.createElement('p');
node.textContent = 'Created';
document.body.appendChild(node);
return {tag: node.tagName, text: node.textContent};
});
console.log(result);
Return plain serializable data rather than a live DOM node; browser objects do not cross the boundary as ordinary Node.js objects.
Troubleshooting
document is not defined
The code ran in Node rather than the page. Put DOM operations inside page.evaluate() (Puppeteer) or the Carlo page script.
Rank #4
“Parent not found” or a null dereference
The selector did not match at insertion time. Check the selector, wait for the parent, and verify that you are on the expected URL and frame.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
$eval throws
No element matched the selector. Use waitForSelector() or a locator, and handle optional elements with a prior existence check.
The text appears as unwanted markup
Use textContent for plain text. Only use HTML parsing when markup is intentional and the source is trusted or sanitized.
The element disappears
A single-page application may re-render its parent and discard manual children. Insert after the application’s render point, use the app’s own state/API where possible, or observe and reapply deliberately.
The screenshot does not show the new node
Capture after the evaluate() promise resolves. If fonts, images, or client rendering are still pending, wait for the relevant selector or page state before taking the screenshot.
Best Value
- Used Book in Good Condition
Performance, reliability, and security notes
- Batch related DOM changes in one
evaluate()call to reduce Node-to-page round trips. - Use a document fragment when adding many siblings, then append the fragment once.
- Keep evaluated functions self-contained and pass only the data they need.
- Do not expose environment variables, filesystem access, or credentials to page code unless the application deliberately requires it.
- Close the browser in a
finallyblock in production so failures do not leak Chrome processes.
Or skip the browser setup
If your goal is a clean image or PDF rather than DOM experimentation, ScreenshotNeo provides a website screenshot API and MCP server. One request can render a URL without maintaining Puppeteer or Carlo.
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}`);
See the ScreenshotNeo documentation for options. Cookie banners, newsletter popups, and chat widgets are removed before the shot; bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Frequently Asked Questions
Can I append a div with Puppeteer?
Yes. Inside page.evaluate(), call document.createElement('div'), set textContent or attributes, and append it to the desired parent.
Does Carlo use a different DOM API?
No. Carlo’s example uses the same browser-standard createElement, textContent, and appendChild operations; its project status is the important distinction.
How do I add HTML rather than plain text?
Use deliberate HTML parsing only for trusted or sanitized markup. For ordinary values, prefer textContent.
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.




