How to Select CSS Pseudo-Elements With Selenium
Selenium cannot return `::before` or `::after` as WebElements. Locate the originating element, then inspect its pseudo-element with JavaScript and getComputedStyle.
Selenium cannot select ::before or ::after as independent WebElement objects. They are CSS pseudo-elements attached to an originating DOM element. Find that real element with a normal Selenium locator, then use browser-side JavaScript and getComputedStyle(element, "::before") or getComputedStyle(element, "::after") to inspect the pseudo-element.
from selenium.webdriver.common.by import By
origin = driver.find_element(By.CSS_SELECTOR, "button.submit")
before_content = driver.execute_script(
'return getComputedStyle(arguments[0], "::before").content',
origin,
)
print(before_content)
The result is the browser-computed CSS value, not necessarily plain visible text. It may be none, a quoted string, a counter expression, or another serialized CSS value. The W3C describes pseudo-elements as bound to an originating element rather than existing independently in the tree (Selectors Level 4). Selenium’s regular element finders locate DOM elements; its getCssValue reads the referenced element’s computed property, so use JavaScript’s second getComputedStyle argument for the pseudo-element.
Why a pseudo-element is not selectable as a WebElement
A CSS selector such as button.submit::before describes the generated pseudo-element in CSS. It does not identify a separate DOM node that Selenium can return. Use button.submit to locate the originating element, then inspect its generated style.
This distinction also separates inspection from interaction: there is no pseudo-element WebElement to click, type into, or wait for. If a test needs to operate a control, locate the real button, link, or input that owns the decoration.
Python: inspect ::before and ::after
With Selenium’s Python binding, pass the originating WebElement as a script argument. WebDriver transfers it into the browser script as arguments[0].
from selenium.webdriver.common.by import By
origin = driver.find_element(By.CSS_SELECTOR, ".status")
before_content = driver.execute_script(
'return getComputedStyle(arguments[0], "::before").content',
origin,
)
after_content = driver.execute_script(
'return getComputedStyle(arguments[0], "::after").content',
origin,
)
before_color = driver.execute_script(
'return getComputedStyle(arguments[0], "::before").color',
origin,
)
print("before:", before_content)
print("after:", after_content)
print("before color:", before_color)
Replace ::before with ::after to inspect the after pseudo-element. To read another property, access it on the returned computed style, such as .color, .display, or .backgroundColor. The browser returns computed values, which may be normalized rather than matching the exact text in the stylesheet.
Wait for the originating element when rendering is asynchronous
If the page inserts the element or applies its classes after load, wait for the origin before reading the pseudo-element. The wait is for a real DOM element; JavaScript then reads its current computed style.
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
origin = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, ".status"))
)
content = driver.execute_script(
'return getComputedStyle(arguments[0], "::before").content',
origin,
)
print(content)
JavaScript: use the Selenium WebDriver binding
The JavaScript binding uses the same browser-side API. This example assumes driver is an initialized Selenium WebDriver instance.
const { By, until } = require('selenium-webdriver');
const origin = await driver.findElement(By.css('.status'));
const beforeContent = await driver.executeScript(
'return getComputedStyle(arguments[0], "::before").content',
origin
);
const afterColor = await driver.executeScript(
'return getComputedStyle(arguments[0], "::after").color',
origin
);
console.log({ beforeContent, afterColor });
For an element that appears later, wait for the real element first:
const origin = await driver.wait(
until.elementLocated(By.css('.status')),
10000
);
const content = await driver.executeScript(
'return getComputedStyle(arguments[0], "::before").content',
origin
);
console.log(content);
See Selenium’s references for finding elements, locator strategies, and the JavaScript binding’s WebElement API.
What the computed content value means
The content property controls generated content. When its computed value is none, the pseudo-element does not generate a box; when it has a value that generates content, the pseudo-element is rendered in relation to its originating element. See the CSS Pseudo-Elements Module Level 4.
"Save"or another quoted value may be serialized with quotation marks. It is a CSS value, not automatically a text-node string.nonemeans there is no generated content for that pseudo-element.normal, counters, attributes, and other CSS forms may not map to a simple literal string.- Generated icons can come from a font or background image while
contentcontains only a glyph or an empty value. Inspect other computed properties when diagnosing appearance.
For a different property, use the same pattern. For example:
display = driver.execute_script(
'return getComputedStyle(arguments[0], "::before").display',
origin,
)
background = driver.execute_script(
'return getComputedStyle(arguments[0], "::before").backgroundImage',
origin,
)
cURL and Python or Node.js outside Selenium
cURL cannot locate a live browser pseudo-element by itself. It fetches HTTP responses, while pseudo-element styles depend on the browser’s DOM, applied CSS, and rendering state. For a remote HTML response, cURL can help inspect source markup or stylesheets, but it cannot substitute for browser-side computed-style inspection. Python and Node.js can run the inspection through Selenium as shown above; without a WebDriver browser session, they face the same limitation.
Or skip the browser setup
If the goal is to capture a rendered page rather than assert on a pseudo-element’s computed property, ScreenshotNeo provides a screenshot API. It does not return a pseudo-element as a Selenium object or replace a computed-style assertion. Its API can capture the rendered page in one request; see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
const image = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', image);
Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed. An MCP server gives AI agents screenshot tools, including take_screenshot, and 1,000 screenshots per month are free with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
NoSuchElementException when locating ::before |
The pseudo-element is being treated as a DOM node. | Find its originating element with a selector such as .status, then pass that WebElement to getComputedStyle. |
getCssValue("content") returns an unexpected value |
Selenium read the originating element’s computed style, not its pseudo-element’s style. | Run getComputedStyle(arguments[0], "::before").content in browser JavaScript. |
Result is none or an empty-looking string |
The wrong origin or pseudo-element may be selected, CSS may set content: none, or the visible decoration may use another property. |
Confirm the locator and inspect both pseudo-elements plus properties such as display, color, and backgroundImage. |
| Value differs from the stylesheet text | Computed CSS is browser-interpreted and can be normalized. | Assert against the expected computed serialization for the supported browser, or test application behavior instead of authored CSS text. |
| Script errors because the element argument is missing | The script ran without passing the located WebElement. | Pass the element after the script string, as the second argument to execute_script. |
| Style is missing or stale | The page may not have applied the class or finished rendering when the read occurred. | Wait for the real element and, if needed, for the application state that applies the relevant class before reading computed style. |
| Test attempts to click generated content | A pseudo-element does not provide an independent interactive target. | Interact with the actual control in the DOM. If the design requires a separate control, it should be represented by real accessible markup. |
Reliability and performance notes
- Locate the originating element once and reuse its WebElement for several property reads while it remains attached to the page.
- Each
execute_scriptcall crosses the WebDriver boundary. Read multiple properties in one script when reducing round trips matters:
styles = driver.execute_script(
'''
const style = getComputedStyle(arguments[0], "::before");
return {
content: style.content,
color: style.color,
display: style.display
};
''',
origin,
)
- If the page replaces the originating node, a previously stored WebElement can become stale. Locate it again after the update.
- Computed styles reflect the current browser state: viewport, loaded stylesheets, classes, and rendering timing can affect results. Keep those conditions stable in tests.
- A cURL request or an HTTP-only parser cannot evaluate browser-generated styles. Use a browser session when the answer depends on rendered CSS.
FAQ
Can Selenium click a ::before element?
No. It is not an independent WebElement. Click the originating interactive DOM element, such as the button.
Does getCssValue support pseudo-elements directly?
It reads a computed property on the element referenced by the WebElement. To read a pseudo-element’s property, call browser-side getComputedStyle with the pseudo-element argument.
Can I read ::marker or another pseudo-element this way?
The same browser API accepts a pseudo-element argument, but the generated style and supported behavior depend on the pseudo-element and browser. Check the binding and browser documentation for the case you need.
Is the returned content the accessible name?
Not necessarily. Generated CSS content and an element’s accessible name are different concerns. Test the semantic DOM and accessibility behavior directly when that is the requirement.


