How to Click an ::after Pseudo-Element with Puppeteer or Pyppeteer
You can’t select ::after as a DOM node. Inspect its host element, then click a verified point in the pseudo-element’s rendered area.

Direct answer: You cannot select a CSS ::after pseudo-element as an ordinary DOM node and call click() on it. Select its originating element instead. If the generated area is separate from the host element’s clickable area, inspect the pseudo-element’s computed styles, identify a point inside its visible, hit-tested area, and click that point with Puppeteer’s or Pyppeteer’s mouse API.
A selector such as .control::after describes generated content in CSS; it does not identify an ElementHandle. A normal element click targets the matched DOM element, usually at its center. That may be sufficient when the host itself handles the click, but it does not guarantee that the generated pixels were clicked. See MDN’s getComputedStyle reference and Puppeteer’s ElementHandle documentation.
1. Decide what should receive the click
Before writing automation, distinguish the host from the visual decoration:

- Click the control: If the pseudo-element is just an arrow, icon, or highlight inside a button, click the real button. Prefer an accessible role or a stable selector for that host.
- Click the generated region: If the test specifically needs the pointer to land on the visible
::afterarea, calculate a point in that area and use a coordinate-based mouse click. - Check whether it can receive the hit: Generated content may be decorative, transparent, clipped, covered by another element, or styled with
pointer-events: none. In those cases, clicking its apparent position may hit the host, another element, or nothing.
The host element’s bounding rectangle is not automatically the pseudo-element’s rectangle. Positioning, transforms, inline layout, writing direction, clipping, and overlapping content all affect where the pixels appear and what receives a pointer event.
2. Puppeteer: inspect the host, then click
This Node.js example launches Chromium, loads a page, inspects the host and its ::after computed styles, and clicks the host. Replace the example URL and selector with your target. The final coordinate click is deliberately shown as a separate step: you must derive and verify the point for your page instead of treating the host rectangle as pseudo-element geometry.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
const selector = '.control';
const locator = page.locator(selector);
await locator.wait();
const info = await page.$eval(selector, el => {
const pseudo = getComputedStyle(el, '::after');
const rect = el.getBoundingClientRect();
return {
content: pseudo.content,
display: pseudo.display,
position: pseudo.position,
width: pseudo.width,
height: pseudo.height,
margin: pseudo.margin,
transform: pseudo.transform,
pointerEvents: pseudo.pointerEvents,
host: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }
};
});
console.log(info);
// If the host is the intended target, click the host itself.
await locator.click();
// If the distinct ::after region is the intended target, inspect the page,
// calculate a point known to lie in that rendered region, then use:
// await page.mouse.click(x, y);
} finally {
await browser.close();
}
})();
Current Puppeteer documentation recommends locators for ordinary DOM interactions. A locator can wait for the host to be present and in a suitable state, including visibility, enabled state, and a stable bounding box. That waiting behavior does not make ::after selectable; the locator still represents the host. See Puppeteer page interactions.
Coordinate click checklist
- Inspect
content,display,position, dimensions, margins, and transforms throughgetComputedStyle(host, '::after'). - Inspect the host’s geometry and the actual page rendering at the same viewport and scroll position used by the test.
- Account for the relevant positioning context, transforms, clipping, and overlap. Do not infer the pseudo-element bounds from the host rectangle alone.
- Confirm that the candidate point is visible and hit-tested as intended. In browser devtools, verify the stacking and pointer-event behavior when the click target is unclear.
- Use
page.mouse.click(x, y)with viewport coordinates. Recompute after scrolling, resizing, or changing device scale or layout.
3. Pyppeteer: inspect computed styles and use the mouse
Pyppeteer exposes similar page evaluation and mouse methods. Its published API reference is version 0.0.25, and its project documentation describes it as an unofficial Python port of Puppeteer. Check the version and browser/runtime compatibility for your project rather than assuming that the older reference describes every current environment. See the Pyppeteer API reference and project documentation.
import asyncio
from pyppeteer import launch
async def main():
browser = await launch(headless=True)
try:
page = await browser.newPage()
await page.goto('https://example.com', {'waitUntil': 'domcontentloaded'})
selector = '.control'
host = await page.querySelector(selector)
if host is None:
raise RuntimeError(f'Host not found: {selector}')
info = await page.evaluate('''el => {
const pseudo = getComputedStyle(el, '::after');
const rect = el.getBoundingClientRect();
return {
content: pseudo.content,
display: pseudo.display,
position: pseudo.position,
width: pseudo.width,
height: pseudo.height,
pointerEvents: pseudo.pointerEvents,
host: {x: rect.x, y: rect.y, width: rect.width, height: rect.height}
};
}''', host)
print(info)
# For the host element, use its selector-based click:
await page.click(selector)
# For the distinct generated area, determine and verify a point first:
# await page.mouse.click(x, y)
finally:
await browser.close()
asyncio.run(main())
page.querySelector() (also known as page.J() in the reference) returns the host element if the selector matches a real DOM node. page.click(selector) scrolls that matched element into view and clicks its center. It is not a pseudo-element click. The mouse API accepts coordinates for the case where you have verified the generated region’s location.
4. Why the obvious approaches fail
| Attempt | Why it fails or misleads | Use this instead |
|---|---|---|
page.$('.control::after') |
CSS pseudo-elements are generated from a host; they are not ordinary DOM elements returned as element handles. | Select .control and inspect its pseudo styles. |
handle.click() on the host |
It clicks the host’s hit area, generally at its center, which may not overlap the generated content. | Use it when the host is the intended target; otherwise click a verified coordinate. |
| Use host rectangle as pseudo rectangle | The generated box can be positioned, transformed, clipped, or laid out separately. | Determine the actual rendered point in the page’s layout. |
Call getComputedStyle and expect a handle |
Computed styles are read-only style data. They describe the pseudo-element but do not create a DOM node. | Use the returned values to guide inspection and coordinate selection. |
Depend on Element.pseudo() |
MDN marks this API experimental with limited availability. | Use host selection and mouse coordinates for a dependable automation pattern. |
The optional second argument to getComputedStyle() identifies the pseudo-element whose resolved styles you want to inspect. For example, getComputedStyle(el, '::after').content reads its generated content property. It remains a style query, not a way to obtain a separate interactive node. Reference: MDN getComputedStyle().
5. Geometry, hit testing, and edge cases
CSS can create pseudo-element boxes in many ways: an absolutely positioned badge, a transformed icon, a border triangle, a marker attached to an inline element, or content that extends beyond an overflow boundary. A single formula based on hostRect.x + pseudo.width cannot cover all of these. The computed left and top values may be relative to a containing block, while transforms and writing modes further affect screen placement.

- No generated box:
content: none,display: none, or zero dimensions can mean there is no visible target to click. - Decorative content: An icon may have no independent action. The host’s event handler is the intended interaction target.
- Pointer-events disabled: With
pointer-events: none, the pointer passes through the pseudo-element to whatever is underneath, often the host. - Overlap and stacking: A sibling, overlay, modal, or sticky header may cover the apparent region. A click at that point can land elsewhere.
- Viewport changes: Responsive CSS can move or resize the pseudo-element. Coordinates become stale after resizing, scrolling, font loading, or layout changes.
- Transforms and scaling: CSS transforms affect rendered geometry. Browser viewport coordinates and screenshot pixels may also differ when device scale factors are involved; use the automation API’s coordinate space and verify the point.
- Generated text: Text wrapping and font metrics can change its bounds. Wait for fonts and layout to settle before deriving coordinates.
If you control the page, make the actual control a semantic DOM element with an accessible name and attach the interaction to it. Keep ::after for decoration. If you are testing a third-party page and must verify a distinct generated region, coordinate clicking is the practical fallback.
6. Waiting and making the interaction reliable
Coordinate automation is inherently tied to a particular rendered state, so synchronize on the state that determines the location. Wait for the host to appear, for relevant content to load, and for animations or transitions to finish. Puppeteer locators handle important readiness checks for a DOM host, but they cannot confirm the independent hit area of its pseudo-element.
- Wait for navigation to the appropriate milestone.
domcontentloadedmay be enough for static CSS, while pages that populate controls after scripts may need a selector wait or an application-specific ready condition. - Wait for the host and inspect computed styles after the page has applied its final classes.
- Disable or wait out animation when it moves the pseudo-element. If the application is yours, a test mode that reduces motion can help.
- Calculate the click point immediately before the action. Avoid reusing coordinates captured before a scroll, resize, content insertion, or responsive breakpoint change.
- After the click, assert the outcome: changed state, navigation, or the expected event result. A dispatched mouse action alone does not prove that the desired target handled it.
A screenshot can help compare what is visible with the coordinate you intend to use. It does not expose pseudo-elements as DOM handles. For a visual record, Puppeteer can capture the page or element; keep the screenshot viewport and device scale consistent with the geometry inspection.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Selector returns no element | The selector targets ::after, the host selector is wrong, or the host has not rendered yet. |
Remove the pseudo-element suffix, select the host, and wait for it to appear. |
| Host click succeeds but the test misses the icon area | The host center is not in the generated region. | Compute and verify a coordinate inside the region, then call the mouse click API. |
| Coordinate click hits another control | Overlay, stacking, scroll offset, stale layout, or wrong coordinate space. | Reinspect at the current viewport and scroll position; check overlap and hit testing immediately before clicking. |
Computed content is none or normal |
The expected stylesheet did not load, a state class is absent, or the pseudo-element is not generated. | Check the host’s active classes, cascade, and loaded stylesheets. |
| It works locally but flakes in CI | Different viewport, font timing, animation, browser version, or resource loading changes layout. | Pin the viewport and browser environment, wait for stable application state, and derive coordinates per run. |
| Pyppeteer launch or protocol errors | The published API reference is old and browser compatibility may differ from the installed runtime. | Check the installed Pyppeteer and Chromium versions and their compatibility before changing click logic. |
| Nothing happens despite visible pixels | The pseudo-element may be decorative, covered, clipped, or set to ignore pointer events. | Check host event behavior, computed pointer events, and the actual topmost hit target. |
8. Performance, reliability, and cost
For a single interaction, querying the host and reading its pseudo styles adds little work compared with launching a browser and loading a page. In a large test suite, avoid launching a new browser for every click; reuse a browser process where the test framework permits, isolate pages or contexts appropriately, and close them after use. Reusing coordinates saves computation but lowers reliability because it assumes the same viewport, scroll position, content, and layout.
Reliability is mostly a layout and synchronization problem. Prefer semantic host interactions whenever that matches user behavior. Reserve coordinate clicks for tests that specifically need the visual region, and validate the resulting application state. Pin browser versions and viewport settings in CI when reproducible geometry matters.
Browser automation has infrastructure costs: browser startup, CPU and memory, navigation time, and any hosted browser minutes or test runner capacity you pay for. The research sources do not establish a universal benchmark or price, so estimate from your own pages, concurrency, and runtime. A screenshot itself does not make the click more reliable; it is a diagnostic artifact.
9. Or skip the browser setup
If your goal is to inspect how a page looks rather than interact with a pseudo-element, ScreenshotNeo is a website screenshot API and MCP server. A GET request returns a PNG, JPEG, WebP, or PDF. The request below captures a page; it does not click controls. See the ScreenshotNeo documentation for API options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
- Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are never billed. Responses identify the page verdict and billing status in headers.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools 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 on every plan.
Sign up for 1,000 free screenshots a month, with no card.
10. FAQ
Can Puppeteer click a CSS generated content element?
It can click a point where the generated content is rendered, using mouse coordinates. It cannot obtain the pseudo-element as a normal DOM handle.
Can I use Element.pseudo() instead?
MDN documents it as experimental with limited availability, so it is not a dependable cross-browser automation foundation: MDN Element.pseudo().
Should I use Puppeteer or Pyppeteer?
Use the runtime your project supports. Puppeteer’s current documentation covers locators; Pyppeteer’s published reference is version 0.0.25, so verify its compatibility against your Python and browser setup.
Does clicking the host count as clicking its ::after?
The pointer may overlap it, but a host click API targets the host’s element area, generally at its center. Confirm the intended coordinate if the generated region itself matters.


