Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

“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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

$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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
The SQL Programming Language: .
  • 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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

An 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.