How to Test Apps That Change CSS Variables with Cypress
Test CSS variable changes in Cypress by checking the update call, the rendered style, or the whole page with a visual baseline.
To test an app that changes CSS variables with Cypress, reproduce the user action, then assert the behavior your feature promises. Spy on document.documentElement.style.setProperty when the update call itself is the contract; assert a CSS property on the affected element when the rendered result is the contract; capture and compare a visual baseline when the whole page must remain visually correct.
Those checks answer different questions. A spy confirms that application code requested a token update. A CSS assertion confirms a particular computed style. A visual comparison can reveal changes to layout or graphics beyond the property you chose to inspect.
1. Choose what the test should prove
| Requirement | Assertion | What it establishes |
|---|---|---|
| The app sets a specific custom property | Spy on setProperty; check the property name and value. |
The call was made with the expected arguments. |
| An element gets a specific rendered style | Assert a CSS property on that element. | The browser reports the expected style for that element. |
| The overall page still looks right | Capture and compare a reviewed visual baseline. | The rendered image remains within the comparison tool’s configured threshold. |
Use the least implementation-specific assertion that proves the requirement. If users care about the visible effect, a spy alone is not enough: the call could occur while the target remains styled incorrectly. If the precise token update is itself important—for example, it is a documented theme contract—checking the call arguments is appropriate.
2. Spy on the CSS variable update
Cypress’s documented Root style recipe spies on document.documentElement.style.setProperty, changes a color input, and checks the call arguments. Adapt the selector, custom property name, and value to your application.
it('updates the page color when the color input changes', () => {
cy.document()
.its('documentElement.style')
.then((style) => {
cy.spy(style, 'setProperty').as('setColor')
})
cy.get('input[type=color]')
.invoke('val', '#ff0000')
.trigger('change')
cy.get('@setColor').should(
'have.been.calledWith',
'--background-color',
'#ff0000'
)
})
This example assumes the app listens for the input’s change event and sets the token on the document root. If your app listens to input, dispatch that event instead. If it sets a property on another element, spy on that element’s style object. The spy must be attached before the interaction that invokes setProperty.
When the property name is deliberately not part of the requirement, match the first argument by type instead. Cypress’s recipe demonstrates using Cypress.sinon.match.string while checking the selected value:
cy.get('@setColor').should(
'have.been.calledWith',
Cypress.sinon.match.string,
'#ff0000'
)
Prefer checking the exact custom property name when it is part of the feature contract. A broad matcher can allow an unintended token to pass unnoticed.
3. Assert the rendered style
When the acceptance criterion is visible behavior, assert the affected element’s CSS property after the interaction. Cypress retries chained assertions while waiting for the expected state.
it('renders the selected page background', () => {
cy.get('input[type=color]')
.invoke('val', '#ff0000')
.trigger('change')
cy.get('main').should('have.css', 'background-color', 'rgb(255, 0, 0)')
})
The expected value here is the browser’s serialized CSS color. A token’s raw value and a rendered value need not have the same representation: the browser resolves styles, inheritance, and color formats when reporting computed CSS. For example, the token may contain a hexadecimal color while the computed property is returned as rgb(...). Assert the output the browser reports for the property you care about.
You can also inspect computed style through the browser API. This is useful when the value is easier to read directly, but keep the assertion focused on the target and property:
cy.get('main').then(($main) => {
const background = getComputedStyle($main[0]).backgroundColor
expect(background).to.equal('rgb(255, 0, 0)')
})
Use a Cypress assertion such as have.css for a straightforward style check; it integrates naturally with Cypress’s retry behavior. A one-time synchronous read inside then does not itself retry if the app has not updated yet. If rendering is asynchronous, use a retrying assertion or wait for an app-specific ready condition before reading.
4. Check the whole page with visual comparison
A property assertion only covers the property and element named in the test. It cannot establish that nearby content did not shift, another component did not pick up the wrong theme, or a graphic still looks right. When the requirement is that the overall appearance remains correct, capture the page in the relevant state and compare it with a reviewed visual baseline.
- Drive the app to a stable state, including the interaction that changes the CSS variable.
- Capture the relevant page or component image using your visual testing setup.
- Compare the capture with the approved baseline using the tool’s configured comparison threshold.
- Review differences. Approve and update the baseline when a change is intentional; investigate unexpected differences.
Keep focused CSS assertions for stable, specific behavior such as a semantic state or a particular background. Add visual comparison when the acceptance criterion covers the combined layout, graphics, or appearance. The two approaches complement each other: a baseline can catch broad visual changes, while a focused assertion explains which behavior matters.
5. Make component tests render like the app
A component mounted on its own may not receive the global styles, reset rules, imported stylesheets, or wrappers it relies on in production. Load the relevant styles and app-level setup through the component test support file or HTML setup so the rendered result reflects the actual application context.
- Include the same global stylesheets and resets that affect the component.
- Reuse app-level providers or wrappers that supply theme state or other required context.
- Keep shared setup in a common module where practical, so component tests do not silently diverge from app startup.
- Use a real browser rendering context when the assertion depends on layout, box dimensions, or overlap.
Cypress’s component testing guidance recommends mirroring the app’s startup setup and loading relevant styles. Browser rendering exposes the real box model for style and interaction assertions; a DOM-only or emulated render may not show layout and overlap behavior the same way.
6. Debug common failures
| Symptom | Likely cause | Fix |
|---|---|---|
The spy reports no setProperty call. |
The spy was installed after the interaction, the app updates a different style object, or the event does not match the app’s listener. | Install the spy first; check whether the app sets the property on the document root or another element; dispatch the event the app handles. |
| The spy passes but the visible style is wrong. | The call happened, but the target may not use that token, a more specific rule may override it, or the app may not have updated the intended element. | Add a rendered-style assertion on the affected element and inspect which token or rule supplies that property. |
| The expected color does not match. | The test compares the token’s source string with the browser’s serialized computed value. | Check the computed property value and use the representation reported by the browser, such as rgb(...). |
| A component test has different styles from the application. | Global styles, resets, imported stylesheets, or app wrappers are missing from component setup. | Load the production-relevant styles and setup in the component support file or HTML setup. |
| A visual comparison changes between runs. | The capture may not be at a stable app state, or the rendered environment differs from the approved baseline. | Wait for an app-specific ready condition, keep test setup consistent, and review diffs before updating the baseline. |
| The CSS assertion fails intermittently after interaction. | The style update is asynchronous, and a one-time read can happen before it completes. | Use a retrying Cypress assertion on the expected CSS property or wait for an application signal that indicates the update is complete. |
7. Keep the test reliable and maintainable
- Spy before acting: attach the spy before triggering the input or control.
- Drive a real interaction: set the control value and dispatch the event the application actually handles.
- Assert the relevant target: check the element whose visible behavior matters, not an unrelated node.
- Use stable selectors: select controls and targets by selectors that represent their test role, rather than incidental styling classes that may change with the theme.
- Match the test scope to the requirement: token call, one computed property, or overall visual result.
- Load production-relevant styles: especially in component tests, where global setup may otherwise be absent.
- Review visual baseline updates: accept intentional differences only after reviewing what changed.
Focused style assertions are usually less costly to maintain than image baselines, but they cover fewer visual details. Visual comparisons require baseline review and consistent capture conditions; they provide broader evidence about the rendered page. Neither method has a universal runtime or cost figure in the sources for this workflow, so choose based on the failure the test needs to catch and the review effort your team can support.
Or skip the browser setup
If you need a rendered screenshot for a visual check, ScreenshotNeo provides a website screenshot API and MCP server. Its API accepts one GET request with a URL and returns an image or PDF. See the ScreenshotNeo API documentation for configuration and response details.
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}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Should I spy on setProperty or inspect the computed style?
Spy on the call when the token update is the contract. Inspect the computed style when the user-visible result is the contract. Use both when you need to prove both steps.
Why does a CSS variable value differ from the element’s CSS value?
The custom property stores a token value, while computed style reports the browser’s resolved value for a specific property and element. Their strings can use different color formats.
When should I add visual regression coverage?
Add it when the requirement covers the full appearance or when changes to several elements, layout, or graphics could matter beyond the one property under test.
Sources
- Cypress recipes: Root style — spying on
setPropertyand triggering the color input change. - Cypress component testing: styling components — setting up app styles and context for component tests.
- Cypress visual testing guide — CSS assertions, image capture, comparison, and baseline review.
- MDN:
Window.getComputedStyle()— reading an element’s computed style.


