Ever needed to make an assertion on a pseudo element in a Cypress test? It’s not as straightforward as you might expect.
Trying to access a pseudo element with cy.get(pseudoElementSelector)
will fail, and if you can’t access it, how can
you test it? In this article, you’ll learn how to access pseudo elements so you can test them in Cypress.
What’s a pseudo-element?
As you can tell from the name, a pseudo element isn’t really an HTML element. It’s a CSS selector that allows you to
target a part of a real element’s content, or to insert CSS-defined content before / after the element’s content. The
most popular examples of pseudo elements are ::before
and ::after
(which allow you to insert content), although
there are a lot more, like
::first-line
, ::first-letter
, and ::marker
.
In everyday work, pseudo elements are mainly used for decorative purposes. They make it easy to add little flourishes to
your markup, like giving the first line of every paragraph a slightly different look, or making irregular shapes from
HTML elements. We’ll look at a couple of examples. Here’s some code that uses ::first-line
and ::first-letter
to
style a web page:
p {
max-width: 50vw;
margin: 15px auto;
line-height: 1.5;
font-size: 1.2rem;
}
p::first-letter {
font-size: 1.5rem;
}
p::first-line {
color: blue;
}
And here’s what the result looks like:

Now here’s an example
from CSS tricks using
::before
and ::after
:
blockquote {
font-style: italic;
line-height: 1.618;
font-size: 1.2em;
width: 30em;
position: relative;
padding: 50px 90px;
}
blockquote::before {
content: open-quote;
top: 0;
left: 0;
}
blockquote::after {
content: close-quote;
bottom: 0;
right: 0;
}
blockquote::before,
blockquote::after {
background-color: #cccccc;
display: block;
width: 60px;
height: 60px;
line-height: 1.618;
font-size: 3em;
border-radius: 100%;
text-align: center;
position: absolute;
}
And here’s its output:

Why can’t cy.get() access pseudo elements?
Pseudo elements are perfectly valid CSS selectors, so it seems a little odd that Cypress can’t access them. What’s going on? The answer is that Cypress uses jQuery under the hood, and the jQuery engine simply doesn’t support selecting pseudo elements.
Support in Playwright
Playwright doesn’t support selecting pseudo elements either. Unlike Cypress, Playwright doesn’t use jQuery, but you
still can’t access pseudo elements in Playwright directly. Why? Pseudo elements aren’t real HTML elements, and thus you
can’t get a JavaScript reference to them. JavaScript treats them as parts of a real element’s content, so you can’t get
a direct JavaScript reference to ::before
or ::after
any more than you can directly select the first space in a p
tag.
Accessing pseudo elements in Cypress
While Cypress doesn’t provide direct support for accessing pseudo elements, not all hope is lost – there is a workaround, and we’ll show it to you here. Here’s the HTML page we’ll be using as the base for our pseudo element tests:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Pseudo Elements Demo</title>
</head>
<body>
<p data-cy="p">
Is your architecture prepared for immersive driver growth? Iteratively touching base about integrating cryptoes
will make us leaders in the company-wide dot-bomb industry.
</p>
<p>
Intelligently engineering effectively best-of-breed brands is crucial to our long-term enterprise. Our business
productizes capabilities to globally and reliably impact our actionable vertical.
</p>
<div class="container">
<blockquote data-cy="block">
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore
magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat.
</blockquote>
</div>
<div class="radio">
<input type="radio" id="radio-1" name="cadet" />
<label for="radio-1" data-cy="radio">
<span>Ender</span>
</label>
</div>
<div class="radio">
<input type="radio" id="radio-2" name="cadet" />
<label for="radio-2" data-cy="radio">
<span>Petra</span>
</label>
</div>
<div class="radio">
<input type="radio" id="radio-3" name="cadet" />
<label for="radio-3" data-cy="radio">
<span>Bean</span>
</label>
</div>
</body>
<style>
body {
background-color: #efefef;
}
p {
max-width: 50vw;
margin: 15px auto;
line-height: 1.5;
font-size: 1.2rem;
}
p::first-letter {
font-size: 1.5rem;
}
p::first-line {
color: blue;
}
.container {
display: flex;
justify-content: center;
align-items: center;
}
blockquote {
font-style: italic;
line-height: 1.618;
font-size: 1.2em;
width: 30em;
position: relative;
padding: 50px 90px;
}
blockquote::before {
content: open-quote;
top: 0;
left: 0;
}
blockquote::after {
content: close-quote;
bottom: 0;
right: 0;
}
blockquote::before,
blockquote::after {
background-color: #cccccc;
display: block;
width: 60px;
height: 60px;
line-height: 1.618;
font-size: 3em;
border-radius: 100%;
text-align: center;
position: absolute;
}
.radio {
color: rebeccapurple;
margin-bottom: 10px;
max-width: 50%;
margin-left: auto;
margin-right: auto;
position: relative;
z-index: 10;
}
.radio label {
display: flex;
align-items: center;
}
.radio input[type="radio"] {
display: none;
}
.radio input + label::before {
content: "";
font: inherit;
color: currentColor;
display: inline-block;
width: 25px;
height: 25px;
border: 0.15em solid currentColor;
border-radius: 50%;
position: relative;
left: -30px;
}
.radio input:checked + label::before {
background-color: currentColor;
}
</style>
</html>
This HTML renders a page that looks like this:

To follow along, simply set up a local dev server that serves this page and add Cypress to that project.
We’ve already established that we can’t directly access pseudo elements from JavaScript, so how are we going to test them? Here’s the approach we’re going to take:
- Get a reference to the parent element of the pseudo element with
cy.get()
. - Use the parent to get a reference to the
window
object, then use thegetComputedStyle
method to read the computed CSS of the pseudo element - Use the
getPropertyValue
method to get the value of the CSS property you want to test - Assert on the returned value.
The following code from Gleb Bahmutov shows this algorithm in action:
cy.get("[data-cy=before-example]")
.scrollIntoView()
.within(() => {
// Cypress does not recognize the selector "p::before"
// because the jQuery engine does not support them
// so we get the content through the computed style
// see https://codepen.io/chriscoyier/pen/Pzzawj
cy.window().then((win) => {
cy.contains("Hello").then(($el) => {
const before = win.getComputedStyle($el[0], "::before");
const beforeContent = before.getPropertyValue("content");
// the content is a string, thus we need to quote it
expect(beforeContent).to.equal('"Greeting"');
});
});
});
And we’re going to adapt this idea to test our web page. Here’s an example of a test that asserts that the color of the
p::first-line
pseudo element on our page is blue:
it("tests the first-line pseudo element", () => {
cy.visit("url-to-your-page");
cy.get('[data-cy="p"]').within(($el) => {
cy.window().then((win) => {
const firstLine = win.getComputedStyle($el[0], "::first-line");
const lineColor = firstLine.getPropertyValue("color");
expect(lineColor).to.equal("rgb(0, 0, 255)");
});
});
});
And here’s a test that checks the width of our blockquote::before
pseudo element:
it("tests the before pseudo element", () => {
cy.visit("url-to-your-page");
cy.get('[data-cy="block"]').within(($el) => {
cy.window().then((win) => {
const before = win.getComputedStyle($el[0], "::before");
const width = before.getPropertyValue("width");
expect(width).to.equal("60px");
});
});
});
Both tests implement the algorithm we outlined above, and in your test runner, you should see both tests pass:

In our final example, we’ll cover how to get Cypress to click on pseudo elements. The radio buttons on the test page are
a little different from normal ones – they’re implemented with ::before
pseudo-elements attached to the label
tags,
instead of being implemented with input
tags. You’re going to write a test that clicks on each ::before
radio
button, and asserts that it has the correct styling after the click.
Add the following code after the previous it
block:
it('clicks the before pseudo elements', () => {
cy.visit('url-to-page')
cy.get('[data-cy="radio"]').each(($el) => {
cy.wrap($el).click(-25, 10)
.within(($el) => {
cy.window().then((win) => {
const before = win.getComputedStyle($el[0], '::before')
const bgColor = before.getPropertyValue('background-color')
expect(bgColor).to.equal('rgb(102, 51, 153)')
})
})
})
This test is largely similar to the first two. It starts by getting all the label
elements with cy.get
, then clicks
on each of them, and asserts on their ::before
pseudo elements. The main difference in this test is the arguments
passed to cy.click()
. By shifting the location of the click to be 25 pixels left and 10 pixels down from the top-left
corner of the targeted element, we are able to get Cypress to click on the pseudo element even though it cannot be
targeted directly.
After running the test, you should see that the toggle has been activated as shown below:

The bright red dot shows the location of the click event, and the test passes, showing that the radio button changed its background color as it was supposed to.
And that’s how you test pseudo elements in Cypress!
Reflect: An alternative to Cypress with native support for pseudo-elements
Reflect is a no-code testing tool that can interact with pseudo-elements like in the example above.
Creating a test in Reflect is easy: the tool records your actions as you use your site and automatically translates those actions into a repeatable test that you can run any time.
Get automated test coverage for your web app today — try it for free.