CSS Testing: How to Automate CSS Regression Tests
CSS testing means checking that your styles still render the way you intended after something changes. The hard part is that almost nothing about a CSS bug is an error: the browser accepts a wrong rule just as happily as a right one, the build stays green, and the pricing table overflows on a 375px screen until a customer notices.
That’s why CSS needs more than one kind of test. This guide covers the four that are worth running — linting, unit tests for CSS logic, computed-style assertions and visual regression tests — with working examples, and then how to automate CSS regression testing across a whole site.
Four ways to test CSS
| Method | What it catches | What it misses | Tools |
|---|---|---|---|
| Linting | Invalid syntax, duplicate properties, selector order problems, rules that break your conventions | Anything that is valid CSS but looks wrong | Stylelint |
| Unit tests for CSS logic | Broken Sass functions and mixins, wrong CSS-in-JS output | How the result renders in a browser | True (Sass), jest-styled-components |
| Computed-style assertions | A specific element losing a specific style | Every style you didn’t write an assertion for | Playwright toHaveCSS |
| Visual regression testing | Layout, spacing, overflow, cascade and breakpoint breakage anywhere on the page | Changes you’ve decided to accept (you approve those) | Playwright toHaveScreenshot, BackstopJS, Diffy |
The first three are cheap and fast, and each covers a narrow slice. Visual regression testing is the one that covers the “valid CSS, wrong result” bugs that make up most real CSS regressions.
1. Lint your CSS with Stylelint
A linter reads your stylesheets without rendering them and flags problems. Stylelint is the maintained standard; the older CSSLint hasn’t had a release since 2016.
npm install --save-dev stylelint stylelint-config-standard
// .stylelintrc.json
{ "extends": "stylelint-config-standard" }
Given this stylesheet:
.card .title { color: #111; }
.title { color: #333; }
.button {
padding: 8px 16px;
padding: 12px 24px;
}
npx stylelint "**/*.css" reports:
styles.css
2:1 × Expected selector ".title" to come before selector ".card .title", at line 1 no-descending-specificity
5:3 × Duplicate property "padding" declaration-block-no-duplicate-properties
Both are the kind of thing that “works” today and bites later: the duplicate padding silently overrides the first, and the specificity order makes it easy to add a rule that never applies. Run Stylelint in your editor and in CI; it takes seconds.
2. Unit test the logic that generates CSS
If your CSS comes out of code, you can test that code like any other.
- Sass: True is a unit-testing library for Sass. You write
describe/itblocks in Sass that assert what a function returns or what CSS a mixin outputs, and run them with Jest, Mocha or Vitest. - CSS-in-JS: jest-styled-components adds a
toHaveStyleRulematcher, so a test can assert that a styled component renders with, say,color: redwhen itsvariantprop isdanger.
These are worth having when you maintain a design-token system or a component library with real logic in it. They don’t render anything, so they can’t tell you whether the page still looks right.
3. Assert computed styles in a real browser
Sometimes one style really matters: the brand color of the primary button, the visibility of a legal notice, the width of a checkout form. Playwright’s toHaveCSS checks the computed value in a real browser, after the cascade has been applied:
import { test, expect } from '@playwright/test';
test('primary button keeps its styles', async ({ page }) => {
await page.goto('/');
const button = page.locator('.button');
await expect(button).toHaveCSS('background-color', 'rgb(29, 78, 216)');
await expect(button).toHaveCSS('padding', '12px 24px');
});
Change the button to a different blue and the test fails with Expected: "rgb(29, 78, 216)", Received: "rgb(30, 64, 175)". Note that colors come back as computed rgb() values, not the hex you wrote.
The limit is obvious once you write a few: you only catch changes to the properties you thought to assert. A new rule that pushes the button off-screen, or shrinks the text next to it, passes.
4. Visual regression testing: the test that catches CSS regressions
Visual regression testing compares screenshots of the same page before and after a change, and flags the pixels that differ. It doesn’t need to know which property changed or why — it sees the result, which is what your visitors see.
It suits CSS unusually well, for three reasons:
- The cascade is global. A change to a shared selector, a utility class or a theme variable can affect pages nobody touched. Screenshots cover pages, not just the component you edited.
- Media queries hide bugs. Rules that only apply below 768px never show up on a developer’s full-width monitor. Capturing at several widths is the only systematic way to see them.
- Updates you didn’t write change CSS too. CMS core, theme and plugin updates ship stylesheets. There’s no code diff of yours to review, but the pages change.
Here’s the minimal version with Playwright, capturing a component at three widths:
import { test, expect } from '@playwright/test';
for (const width of [375, 768, 1440]) {
test(`navigation at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await page.goto('/');
await expect(page.locator('nav')).toHaveScreenshot(`nav-${width}.png`);
});
}
The first run stores nav-375-linux.png, nav-768-linux.png and nav-1440-linux.png as baselines; every later run compares against them and fails with a diff image when the navigation changes. Our Playwright visual regression testing guide covers the rest of a solid setup: stable screenshots, masking dynamic content, thresholds and running it in CI.
Automating CSS regression testing across a whole site
Component tests are a good start. CSS regressions, though, are site-wide by nature, so the automated check that pays off most is a screenshot of every important page, at every breakpoint, compared before and after each change. Getting that right comes down to four decisions:
- Which pages. One of each template (home, article, listing, product, form, checkout) catches most template-level CSS bugs. Add pages with unusual content: very long titles, tables, embeds.
- Which breakpoints. At least one per media-query range you support — typically a phone, a tablet and a desktop width. See our guide to responsive design testing for choosing them.
- When it runs. On every pull request that touches styles, before every deploy, and after every CMS, theme or plugin update — the last one is where most unreviewed CSS changes come from.
- What it compares against. Either the last approved baseline, or another environment: staging against production is the most useful comparison before a release, because it compares the exact code you’re about to ship with what is live.
Then deal with noise before it trains people to ignore the results. Dates, carousels, ads and cookie banners change between runs without anything being broken; mask or hide them, or the tool will flag them every time. Our post on avoiding false positives in visual testing goes through the usual culprits.
Doing it with Diffy
Diffy is built for exactly this site-wide check, without writing test code. You give it a list of pages and breakpoints; it screenshots each page on production, staging, development or a pull request environment, in Chrome or WebKit, and shows you what changed:
- Compare environments or before/after. Staging against production before a release, or the same site before and after a CMS or plugin update.
- CSS changes that shift content don’t flood the report. Diffy’s comparison algorithm recognizes vertical shifts, so a header that grew by one line shows up as one change rather than every element below it.
- Noise handling in project settings. Mask regions, remove elements, replace dynamic text with fixed content, or inject custom CSS and JavaScript before each capture.
- Review built for many screenshots. Approve from thumbnails, and the same change repeated across pages is grouped so you approve it once.
- Runs where your workflow is. On a schedule, on demand, or from CI through the CLI and API (API access is on paid plans), with results posted to GitHub and GitLab pull requests.
The free plan includes 500 screenshots a month — enough to cover a small site’s key templates at three breakpoints on a regular schedule. If you’d rather keep everything in code, Diffy vs BackstopJS and Diffy vs Playwright compare the trade-offs honestly.
Which CSS testing method should you use?
Use all of them at the level each is cheap:
- Always: Stylelint in the editor and CI. It costs nothing and removes a class of bugs.
- If you have CSS logic: True or jest-styled-components for design tokens, mixins and styled components.
- For a few critical styles: a handful of
toHaveCSSassertions — not hundreds. - For everything else: visual regression tests across your key pages and breakpoints, run on pull requests, before deploys and after updates.
FAQ
What is CSS regression testing? Checking that a change — to your stylesheets, a component, a theme, a plugin or a CMS update — hasn’t changed how existing pages look in ways you didn’t intend. Because most CSS bugs are visual rather than syntax errors, it’s usually done by comparing screenshots of the same pages before and after the change, at every breakpoint you support.
Can you unit test CSS?
Partly. You can unit test the logic that produces CSS: Sass functions and mixins with True, or CSS-in-JS output with jest-styled-components and its toHaveStyleRule matcher. You can also assert individual computed styles in a real browser with Playwright’s toHaveCSS. None of these tell you whether the page still looks right as a whole, which is why most teams add visual regression tests on top.
What is the best tool for CSS testing?
There’s no single tool, because the layers catch different bugs. Stylelint catches invalid and risky CSS before it ships. Playwright’s toHaveCSS pins down a handful of critical styles. A visual regression tool — Playwright’s toHaveScreenshot, BackstopJS, or a hosted service such as Diffy — catches the layout and cascade breakage the other two miss.
Is CSSLint still maintained? No. The last csslint release on npm, 1.0.5, was published in December 2016. Stylelint is the actively maintained linter most projects use today.
How do I test CSS across browsers and screen sizes? Run the same checks at each viewport width you support and in each browser engine you care about. Media queries only apply at certain widths, so a single desktop screenshot misses most responsive bugs. Playwright can emulate viewports and run Chromium, Firefox and WebKit; Diffy captures every page at each breakpoint you configure, in Chrome or WebKit.
Try it on your site
Create a free Diffy account, add your key pages and three breakpoints, and run a comparison before your next CSS change ships. New to the practice? Start with what visual regression testing is.