# Quickstart
Your CI takes the screenshots. stateofpixel compares them with the last approved ones and sets a check on the pull request until someone approves the changes.
Pick where your screenshots come from, or follow the steps below. They use GitHub Actions and need no secret.
- [Playwright: Add the reporter. It uploads when the run ends.](https://stateofpixel.com/docs/playwright.md)
- [Storybook: One command captures every story at each width.](https://stateofpixel.com/docs/storybook.md)
- [Any screenshots: Point the CLI at a folder of PNG files.](https://stateofpixel.com/docs/any-screenshots.md)
- [Try it locally: Compare two folders. No account needed.](https://stateofpixel.com/docs/cli.md)
To let a coding agent do the setup, give it this prompt:
Set it up with your coding agent
```
Add visual regression testing to this repository with stateofpixel.
Read https://stateofpixel.com/llms.txt, then the quickstart it links, and follow it:
1. Find where the tests write screenshots, or set up Playwright or Storybook capture.
2. Add the upload step to the GitHub Actions workflow with id-token: write and fetch-depth: 0.
3. Run it on pushes to main and on pull requests.
Do not add a secret. Tell me which command takes the screenshots.
```
## 1. Install the GitHub App
Sign in on [stateofpixel.com](https://stateofpixel.com/index.md) and install the GitHub App on your account or organization. Pick the repositories to test. Each repository becomes a project, and its project page shows the setup steps until the first build arrives.
## 2. Add the workflow
Add a step that runs after your tests write their screenshots. This example runs Playwright tests that save PNG files into `screenshots`:
```yaml .github/workflows/visual.yml
name : Visual
on :
push :
branches : [ main ]
pull_request :
permissions :
contents : read
id-token : write
jobs :
visual :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- uses : actions/setup-node@v4
with :
node-version : 22
- run : npm ci
- run : npx playwright install --with-deps chromium
- run : npx playwright test
- run : npx stateofpixel upload screenshots
```
- `id-token: write` lets the CLI sign in with the GitHub Actions OIDC token, so you do not need a secret. See [Security](https://stateofpixel.com/docs/security.md#how-ci-signs-in).
- Run it on pushes to `main` too. Builds on your default branch are approved on their own and become the baseline that pull requests compare against.
- `fetch-depth: 0` gives the CLI the git history it uses to find the baseline. With a shallow clone the server asks the GitHub compare API instead.
With the Playwright integration you skip the upload step, and with Storybook one command captures every story. See [Playwright](https://stateofpixel.com/docs/playwright.md) and [Storybook](https://stateofpixel.com/docs/storybook.md).
## 3. Push to main
The first build has nothing to compare with, so every snapshot is added and the build is approved on its own. It becomes the first baseline, and the check says "Baseline created".
## 4. Open a pull request
When a pull request changes how something looks, the `stateofpixel` check waits with "2 changes to review". Details opens the build page, where anyone with write access to the repository approves or rejects each change. When every change is approved, the check turns green. See [Reviewing changes](https://stateofpixel.com/docs/review.md).
## 5. Require the check
The check only blocks merges when GitHub requires it. In the repository settings on GitHub, add a branch protection rule or a ruleset for `main` that requires status checks to pass, and pick `stateofpixel`. See [The GitHub check](https://stateofpixel.com/docs/checks.md) for every state it can be in.
Pull requests from forks never get the check, because GitHub gives them no token to sign in with. Once the check is required, they cannot merge until an admin bypasses the rule. See [No check on the pull request](https://stateofpixel.com/docs/troubleshooting.md#no-check-on-the-pull-request).
## All docs
### Get started
- [Quickstart](https://stateofpixel.com/docs.md): Set up stateofpixel on a GitHub repository in a few minutes.
- [Moving from another tool](https://stateofpixel.com/docs/moving.md): Switch to stateofpixel from Chromatic, Argos, Percy or Lost Pixel.
### Capture
- [Playwright](https://stateofpixel.com/docs/playwright.md): Take snapshots in Playwright tests and upload them with the stateofpixel reporter.
- [Storybook](https://stateofpixel.com/docs/storybook.md): Capture every story of a built Storybook and upload it with one command.
- [Any screenshots](https://stateofpixel.com/docs/any-screenshots.md): Upload a folder of PNG files from any test runner.
### Run in CI
- [Other CI](https://stateofpixel.com/docs/other-ci.md): Run stateofpixel on CI other than GitHub Actions with a project token.
- [Sharding](https://stateofpixel.com/docs/sharding.md): Split a visual test run across CI jobs and get one build and one check.
- [Suites](https://stateofpixel.com/docs/suites.md): Run separate visual suites in one repository, each with its own baselines and check.
### Review
- [Reviewing changes](https://stateofpixel.com/docs/review.md): Compare, approve and reject visual changes on the build page.
- [The GitHub check](https://stateofpixel.com/docs/checks.md): What the stateofpixel check on a commit means and how to require it.
- [Baselines](https://stateofpixel.com/docs/baselines.md): How stateofpixel picks the build a new build is compared against.
### Guides
- [Stable screenshots](https://stateofpixel.com/docs/stable-screenshots.md): Keep screenshots the same between runs so only real changes need review.
- [Troubleshooting](https://stateofpixel.com/docs/troubleshooting.md): Fixes for missing checks, failed uploads, stuck builds and other common problems.
### Manage
- [Accounts and projects](https://stateofpixel.com/docs/accounts.md): Accounts, members, project settings, and what happens when you delete a project or uninstall the app.
- [Billing](https://stateofpixel.com/docs/billing.md): Upgrade, change or cancel a plan, and what happens when a payment fails.
- [Security](https://stateofpixel.com/docs/security.md): How CI signs in, who can see a project, and how screenshots and tokens are stored.
### Reference
- [CLI](https://stateofpixel.com/docs/cli.md): Commands, flags, environment variables and exit codes of the stateofpixel CLI.
- [Limits and storage](https://stateofpixel.com/docs/limits.md): Limits per build and account, how storage is counted, and how long images are kept.
---
# Moving from another tool
stateofpixel compares PNG files your CI takes. Moving over means making your CI write PNG files, then replacing the upload step.
## What carries over
Baselines do not carry over. The first build on your default branch has nothing to compare with, so every snapshot is added and the build becomes the first baseline. Run the new workflow on your default branch once before you rely on the check in pull requests.
Snapshot names come from file paths, so keep the paths stable. A folder `checkout/empty-cart.png` becomes the snapshot `checkout/empty-cart`. See [Snapshot names](https://stateofpixel.com/docs/any-screenshots.md#snapshot-names).
## Chromatic and Lost Pixel with Storybook
Replace the Chromatic or Lost Pixel step with a Storybook build and one capture command:
```yaml .github/workflows/visual.yml
permissions :
contents : read
id-token : write
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- run : npm ci
- run : npx playwright install --with-deps chromium
- run : npx storybook build
- run : npx stateofpixel storybook storybook-static --viewports 375,1280
```
Screenshots render in Chromium on your CI, not in the other tool's browsers. Remove the project token secret, since GitHub Actions signs in with OIDC. For Lost Pixel, also delete `.lostpixel/baseline` from the repository. See [Storybook](https://stateofpixel.com/docs/storybook.md).
## Argos and Percy with Playwright
Replace `argosScreenshot()` or `percySnapshot()` with `snapshot()` from `stateofpixel/playwright`, and add its reporter. The reporter uploads when the run ends, so the separate upload step or `percy exec` goes away. See [Playwright](https://stateofpixel.com/docs/playwright.md).
## Any other folder of images
If you upload a folder of PNG files with `argos upload` or `percy upload`, upload the same folder instead:
```sh terminal
npx stateofpixel upload screenshots
```
See [Any screenshots](https://stateofpixel.com/docs/any-screenshots.md).
## Check the result before you switch
`npx stateofpixel compare
` compares two folders on your machine without an account and writes an HTML report. Use it to see how screenshots from your old tool compare with new ones. See [compare](https://stateofpixel.com/docs/cli.md#compare).
Each comparison page lists what changes in more detail: [Chromatic](https://stateofpixel.com/compare/chromatic.md), [Argos](https://stateofpixel.com/compare/argos.md), [Percy](https://stateofpixel.com/compare/percy.md) and [Lost Pixel](https://stateofpixel.com/compare/lost-pixel.md).
---
# Playwright
Call snapshot() in your tests. The reporter uploads the screenshots when the run ends.
## Install
```sh terminal
npm install -D stateofpixel
```
## Add the reporter
```ts playwright.config.ts
import { defineConfig } from "@playwright/test" ;
export default defineConfig ({
reporter: [
[ "list" ],
[ "stateofpixel/playwright" , { buildName: "e2e" }],
] ,
}) ;
```
The reporter clears `stateofpixel-screenshots` when the run begins and uploads it when the run ends. It uploads on CI only, when `CI` is set. A local run leaves the screenshots in the folder so you can look at them.
## Take snapshots
```ts tests/pricing.spec.ts
import { test } from "@playwright/test" ;
import { snapshot } from "stateofpixel/playwright" ;
test ( "pricing" , async ({ page }) => {
await page. goto ( "/pricing" );
await snapshot (page, "Marketing/Pricing" );
await snapshot (page, "Marketing/Pricing fold" , { fullPage: false });
});
```
`snapshot(page, name)` waits for fonts, disables animations, hides the caret and saves a full-page screenshot. Pass `{ fullPage: false }` to capture only the viewport.
The browser and viewport width are added to the name, so `Marketing/Pricing` in Chromium at 1280px becomes `Marketing/Pricing [chromium 1280]`. Running the same test in several Playwright projects gives one snapshot per browser and width. The name is how stateofpixel matches snapshots across builds, so renaming a snapshot makes it a new one.
Next to each PNG, `snapshot` writes the browser, viewport, Playwright project, test file and line. The build page shows them under the snapshot.
## Run it on CI
```yaml .github/workflows/visual.yml
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- run : npm ci
- run : npx playwright install --with-deps chromium
- run : npx playwright test
```
No upload step is needed. The workflow still needs `id-token: write`, as in the [Quickstart](https://stateofpixel.com/docs.md). With Playwright sharding (`--shard 1/4`), the reporter uploads once per shard and the build finishes when the last shard is done.
When a test fails, the upload is marked as a subset. Snapshots of tests that did not run are then not reported as removed. A run you stop, for example with Ctrl+C, uploads nothing. When the upload fails, the reporter prints the error and marks the Playwright run as failed.
## Reporter options
| Option | What it does |
| ----------------- | ------------------------------------------------------------------------------------------------------- |
| `buildName` | Separate suite with its own baselines and check. See [Suites](https://stateofpixel.com/docs/suites.md). |
| `baselineBranch` | Branch to compare against. |
| `threshold` | Color difference threshold from 0 to 1. Overrides the project setting. |
| `subset` | Always mark the upload as a subset, so missing snapshots are never removed. |
| `strict` | Fail the run instead of skipping the upload on an outage, a rate limit or a pull request from a fork. |
| `uploadOutsideCi` | Upload from your machine too. Needs `STATEOFPIXEL_TOKEN`. |
| `nonce` | Id shared by every shard of one build. |
`STATEOFPIXEL_DIR` changes the folder that `snapshot` writes to and the reporter uploads.
---
# Storybook
One command opens every story of a built Storybook in Chromium, takes a screenshot at each width and uploads them.
## Install
The capture uses the Playwright in your project and needs Storybook 7 or newer, which writes the `index.json` the CLI reads.
```sh terminal
npm install -D stateofpixel playwright
npx playwright install chromium
```
## Run it on CI
```yaml .github/workflows/visual.yml
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- run : npm ci
- run : npx playwright install --with-deps chromium
- run : npx storybook build
- run : npx stateofpixel storybook storybook-static --viewports 375,1280
```
Keep `id-token: write` and the push trigger on `main` from the [Quickstart](https://stateofpixel.com/docs.md).
Each story is captured at every width in `--viewports` and named `Title/Name [chromium 1280]`, like `Button/Primary [chromium 375]`.
## Pick stories and wait for them
```sh terminal
npx stateofpixel storybook storybook-static \
--include "Components/**" \
--exclude "**/Playground" \
--wait-for-selector "[data-ready]" \
--delay 200
```
| Flag | Default | What it does |
| -------------------------------- | --------------------- | -------------------------------------- |
| `--viewports ` | `1280` | Comma separated viewport widths. |
| `--include ` | | Only stories whose title/name match. |
| `--exclude ` | | Skip stories whose title/name match. |
| `--wait-for-selector ` | `#storybook-root > *` | Wait for this before each screenshot. |
| `--delay ` | `0` | Wait this long before each screenshot. |
Each story opens in Chromium at a height of 720px, with reduced motion and a device scale factor of 1, and the screenshot covers the full page. A story has 15 seconds to load and to match `--wait-for-selector`. When a story does not render, the command lists it and stops before anything uploads.
In `--include` and `--exclude`, `*` stays inside one segment of `Title/Name` and `**` crosses segments.
`storybook` also takes every flag of `upload`, like `--build-name`, `--shard` and `--threshold`. With `--shard 2/4`, it captures only every fourth story, starting at the second, so each CI job takes its share. See the [CLI](https://stateofpixel.com/docs/cli.md) reference. Use `--dry-run` to capture and print the plan without uploading.
Stories that load data or fonts late are the usual cause of noisy diffs. [Stable screenshots](https://stateofpixel.com/docs/stable-screenshots.md) has the fixes.
---
# Any screenshots
Anything that writes PNG files works: Cypress, BackstopJS, a script, or screenshots of a native app.
## Upload a folder
Run `npx stateofpixel upload screenshots` after the step that writes the files, as in the [Quickstart](https://stateofpixel.com/docs.md). The CLI hashes every PNG and uploads only the images the server does not have yet.
## Snapshot names
A snapshot's name is its path inside the folder without `.png`:
```
screenshots/
header.png header
checkout/empty-cart.png checkout/empty-cart
checkout/empty-cart.meta.json metadata for checkout/empty-cart
```
The name is how stateofpixel matches a snapshot with the same snapshot in the baseline. Keep names stable between runs. Renaming a file makes it a new snapshot, and the old one shows as removed. If you capture several browsers or widths, put them in the name, like `header [chromium 1280].png`.
## Metadata
A `.meta.json` file next to a PNG is sent as that snapshot's metadata and shown on the build page. It is for display only and never changes how snapshots match. Up to 4 KB per snapshot.
```json checkout/empty-cart.meta.json
{
"browser" : "chromium" ,
"viewport" : 1280 ,
"testFile" : "tests/cart.spec.ts"
}
```
## Partial runs
When only some tests ran, pass `--subset`. Snapshots that are missing from the folder are then not reported as removed.
## Compare on your machine
`compare` diffs two folders and writes an HTML report. It needs no account and uploads nothing. The report lists the snapshots and has the same Side by side, Diff, Slider and Flip views as the build page, with `j`, `k` and `1` to `4`.
```sh terminal
npx stateofpixel compare screenshots baseline --threshold 0.2
# Writes stateofpixel-report/index.html
```
---
# Other CI
Outside GitHub Actions, the CLI signs in with a project token instead of the OIDC token.
## Create a token
Repository admins open the project on stateofpixel.com, then Settings, Tokens, and create a token. It starts with `sop_` and is shown once, so copy it into your CI's secrets right away. Tokens can be revoked from the same place, which also shows when each one was last used.
## Run the CLI
Set the token as `STATEOFPIXEL_TOKEN` and run the same commands as on GitHub Actions:
```
export STATEOFPIXEL_TOKEN = sop_...
npx playwright test
npx stateofpixel upload screenshots
```
## Git information
On other CI the CLI reads the commit, branch and history from the local checkout:
- Check out the branch by name. On a detached `HEAD` the branch is recorded as `HEAD`.
- Fetch enough history to reach your default branch. When the baseline is not in the local history, the server asks the GitHub compare API.
- The baseline branch is `origin/HEAD`, or `main` when that is not set. Pass `--baseline-branch` to pick another one.
- The commit has to be on GitHub, because the check is set on it there.
The pull request number comes from the GitHub Actions event, so on other CI builds are linked to their branch and commit only. A new push does not carry over approvals from the last build, and builds are not marked superseded.
## Shards
GitHub Actions gives every job of a run the same id. Elsewhere, set `STATEOFPIXEL_NONCE` to an id your CI shares between the jobs of one run. See [Sharding](https://stateofpixel.com/docs/sharding.md).
---
# Sharding
Several CI jobs can upload parts of one build. The check reports once, after the last part.
## When you know the shard count
Each job passes its shard and the total. The first job to upload creates the build, the others join it, and the build finishes when every shard is done.
```yaml .github/workflows/visual.yml
jobs :
visual :
runs-on : ubuntu-latest
strategy :
matrix :
shard : [ 1 , 2 , 3 , 4 ]
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- run : npm ci
- run : npx playwright install --with-deps chromium
- run : npx playwright test --shard ${{ matrix.shard }}/4
- run : npx stateofpixel upload screenshots --shard ${{ matrix.shard }}/4
```
While shards upload, the check says "Waiting for screenshots (2 of 4 shards)". The [Playwright reporter](https://stateofpixel.com/docs/playwright.md) reads Playwright's own `--shard`, so with it you skip the upload step. [Storybook](https://stateofpixel.com/docs/storybook.md) capture splits the stories by the same `--shard`.
## When you do not
Each job uploads with `--shard auto`, and one last job calls `finalize`:
```yaml .github/workflows/visual.yml
jobs :
visual :
# ... each job ends with:
steps :
- run : npx stateofpixel upload screenshots --shard auto
finalize :
needs : visual
if : always()
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
with :
fetch-depth : 0
- run : npx stateofpixel finalize --skip-if-empty
```
`--skip-if-empty` makes `finalize` report a build with no changes when no shard ran, so the check still reports.
## How shards find each other
Shards of one build share a nonce. On GitHub Actions it is the run id plus the attempt, so every job of a run joins the same build, and a re-run starts a fresh build instead of mixing with the old one. Elsewhere, set it yourself:
```
export STATEOFPIXEL_TOKEN = sop_...
export STATEOFPIXEL_NONCE = " $CI_PIPELINE_ID "
npx stateofpixel upload screenshots --shard 2/4
```
## Builds that never finish
A build that is not finished 60 minutes after its first shard expires. The check shows "Build never finished". See [how to recover](https://stateofpixel.com/docs/troubleshooting.md#the-check-says-build-never-finished). A build takes up to 256 shards.
---
# Suites
One repository can run several suites, like end-to-end tests and Storybook. Each suite has its own baselines and its own check.
## Name the suite
Pass `--build-name` to `upload` or `storybook`, or `buildName` to the [Playwright reporter](https://stateofpixel.com/docs/playwright.md):
```yaml .github/workflows/visual.yml
steps :
- run : npx playwright test
- run : npx stateofpixel upload screenshots --build-name e2e
- run : npx storybook build
- run : npx stateofpixel storybook storybook-static --build-name storybook
```
Without a name the suite is `default` and its check is `stateofpixel`. Other suites report as `stateofpixel/`, like `stateofpixel/storybook`. Require each one on GitHub if it should block merges.
## Baselines per suite
Snapshots only compare with the same suite, so the same snapshot name in two suites never collides. Run every suite on pushes to your default branch too, so each one gets its own baseline.
---
# Reviewing changes
Every build has a page that lists its snapshots next to the baseline. The check on the pull request links to it.
## The builds list
The project page lists its builds, newest first. Click the Build column header to put the oldest first. Filter picks which states show. By default it hides builds with no changes and expired builds, so the list shows what needs attention. Default and All at the top of the menu reset it.
Click a branch or a pull request number in a row to show only its builds. Each filter shows as a chip you can clear, and Clear filters removes them all. Filters and the order are part of the address, so a filtered list can be shared.
## The build page
The sidebar groups snapshots into Changed, Added, Removed, Failed and Unchanged. The first changed snapshot opens when the page loads, and each snapshot has its own link you can share. Drag the edge of the sidebar to resize it, and double-click the edge to reset it.
- **Changed**: pixels differ from the baseline above the threshold, or the size changed.
- **Added**: the baseline has no snapshot with this name.
- **Removed**: the baseline has it and this build does not. Removed snapshots never need review.
- **Failed**: the upload or the diff failed on CI. A build with a failed snapshot cannot pass, so push again after fixing the cause.
## Compare
- **Side by side**: baseline left, new right, with the diff drawn over the new image. Press `d` to hide it.
- **Diff**: the new image with the changed pixels in green. Press `d` for Diff only, which shows the changed pixels without the image.
- **Slider**: drag a handle to wipe between the two.
- **Flip**: one frame that switches between baseline and new when you press `space`. The best mode for 1 pixel shifts.
The diff is green by default. While it shows, the color dots in the toolbar switch it to red, magenta or blue. The color stays as you move between snapshots and goes back to green when you reload the page.
Images show at Fit by default. Scroll or drag to pan, and pinch, or scroll with `ctrl` held, to zoom. You can also zoom with `+` and `-`, and `0` shows real pixels at 100%. Fit, or `f`, resets the view. In Side by side both images pan and zoom together. When the sizes differ, both images align at the top left.
## Approve and reject
Anyone with write access to the repository on GitHub can review. See [Who can review](#who-can-review).
- Approve a snapshot with `a`, which also moves to the next pending one, or approve every pending snapshot with Approve all.
- Reject with `r` and an optional comment. One rejection makes the check fail with "1 change rejected". Reject build rejects every pending snapshot at once.
- Undo a review with `u`.
When every change is approved, the check turns green on GitHub. See [The GitHub check](https://stateofpixel.com/docs/checks.md).
## Snapshot details
Under the image, the snapshot shows what else is known about it:
- **Review**: who approved or rejected it and when, with the comment, or the build an approval carried over from.
- **Looks flaky**: shown when the snapshot changed back and forth on the default branch, or another build of the same commit has a different image. See [Find flaky snapshots](https://stateofpixel.com/docs/stable-screenshots.md#find-flaky-snapshots).
- **History**: the recent builds on the default branch where the image changed. History opens the snapshot on the Baselines tab. See [Baselines](https://stateofpixel.com/docs/baselines.md).
- **Details**: the metadata your test sent, like browser, viewport and test file.
## Who can review
Access follows the repository on GitHub, and there are no seats:
- Read access opens builds and baselines.
- Write access, including maintain and admin, approves and rejects changes. Everyone else sees "You need write access on GitHub to review".
- Admin access opens the project settings.
stateofpixel asks GitHub for your permission again when you open a project and the last check is more than 5 minutes old, so a change on GitHub applies within about 5 minutes. If that check cannot run, the last known permission is kept for up to 15 minutes. Links to images of a private project that were already shown keep working for up to 2 hours.
Roles on the account, which decide who changes the plan, are read from GitHub each time you load the site. Changing the plan always checks with GitHub first.
## New pushes
A new push on the pull request makes a new build, and the older build is marked superseded, with a link to the newest one. A superseded build cannot be reviewed, so review the newest one. Approvals carry over: if an image was approved in an earlier build of the same pull request and suite, it is approved again, and the page says who approved it and in which build. So a rebase does not ask you to review the same pixels twice.
Rejections do not carry over. A rejected image that shows up again is pending, with a note that it was rejected before.
## Keyboard shortcuts
Press `?` on the build page to see them.
| Action | Keys |
| ----------------------------------------------- | --------------- |
| Next / previous snapshot | `j` `k` |
| Focus filter | `/` |
| Show shortcuts | `?` |
| Approve, move to next pending | `a` |
| Reject with a comment | `r` |
| Undo review | `u` |
| Approve all pending | `shift` `a` |
| Side by side, Diff, Slider, Flip | `1` `2` `3` `4` |
| Diff overlay in Side by side, Diff only in Diff | `d` |
| Toggle image in Flip mode | `space` |
| Fit / 100% zoom | `f` `0` |
| Zoom in / out | `+` `-` |
---
# The GitHub check
Every build sets a commit status on its commit. Details on GitHub opens the build page.
## States
| Build | Check | Text |
| -------------------------- | ------- | --------------------------------------- |
| Shards still uploading | Pending | Waiting for screenshots (2 of 4 shards) |
| Changes to review | Pending | 12 changes to review |
| Nothing changed | Success | No visual changes |
| Every change approved | Success | 12 changes approved |
| First build of a suite | Success | Baseline created, 40 snapshots |
| Default branch | Success | Baseline updated, 12 changes |
| A change rejected | Failure | 2 changes rejected |
| Not finished in 60 minutes | Error | Build never finished |
| Upload failed on CI | Error | Upload failed, see CI logs |
| Over the storage limit | Success | Storage limit reached, not compared |
A build with changes to review stays pending until someone approves or rejects them, however long that takes. When the storage limit is reached, builds pass without being compared, so CI keeps passing. See [Limits and storage](https://stateofpixel.com/docs/limits.md).
## Check name
The check is `stateofpixel`. Each extra suite gets its own, like `stateofpixel/storybook`. See [Suites](https://stateofpixel.com/docs/suites.md).
## Require it
The CLI exits 0 when there are changes, so the CI job passes and the check decides. It blocks a merge only when GitHub requires it:
1. Open the repository's settings on GitHub and add a branch protection rule or a ruleset for your default branch.
2. Turn on required status checks and add `stateofpixel`, plus the check of each suite that should block.
A pending check blocks the merge too, so a pull request with changes nobody reviewed cannot merge. Changes that land without review still become the baseline on the default branch. See [Baselines](https://stateofpixel.com/docs/baselines.md).
Pull requests from forks get no check, because GitHub gives them no token to sign in with. See [No check on the pull request](https://stateofpixel.com/docs/troubleshooting.md#no-check-on-the-pull-request).
## GitHub App permissions
The app uses Commit statuses write to set the check, Pull requests read for the pull request number, base branch and squash merges, Contents read for the compare API, and Metadata read, which every app has. It also asks for Pull requests write, Checks write and Actions read, which it does not use.
---
# Baselines
A build is compared with the newest approved build in its own git history. Your default branch keeps that history approved.
## How the baseline is picked
1. The CLI sends the commit's history, up to 100 commits. The server walks it from the newest commit and takes the first build of the same suite that was approved or had no changes.
2. If none matches, for example after a shallow clone, the server asks GitHub which of the newest builds on the baseline branch are in the commit's history, and takes the newest.
3. If there is still none, the build has no baseline. Every snapshot is added and the build is approved on its own, so it becomes the first baseline. The build page shows "First build, no baseline" next to Baseline.
For a new pull request, the baseline is usually the last build on the branch it started from. After you approve a build on the pull request, the next push compares with that build, so you only review what changed since.
Builds uploaded with `--subset` and builds over the storage limit are never baselines.
## The default branch
Builds on your default branch are approved on their own and become the newest baseline. Whatever lands there is the truth, reviewed or not. To add other branches, like `release/*`, edit Auto-approve branches in the project settings. `*` matches inside one path segment and `**` across segments. It applies to builds that are not on a pull request.
## After a merge
Squash, rebase and merge commits all work. After a squash or rebase merge the new commit on the default branch is not a descendant of the pull request, so stateofpixel asks GitHub which pull request it came from. The build page shows "From PR #123" with a link to the pull request's last build, and marks changes that were never approved on that pull request as "not reviewed on PR".
## The Baselines tab
The project's Baselines tab shows the newest approved image of every snapshot on the default branch, per suite. Type in the search box to show the snapshots whose name starts with that text. It matches upper and lower case exactly. With more than one suite, the Suite menu picks one.
Open a snapshot to see its history: every build on the default branch where its image changed, and who approved it when that is known. Pick two builds to compare them in the viewer, the older one as the baseline. The two newest are picked when the page opens.
See [Reviewing changes](https://stateofpixel.com/docs/review.md) for how approvals carry over between pushes.
---
# Stable screenshots
A snapshot that changes on every run asks for review on every run. Most noise comes from a few causes, and each has a fix.
## Find flaky snapshots
A changed snapshot shows "Looks flaky" in the build page when its image changed at least twice over the last 10 builds on the baseline branch and went back to an image it had before. It also shows when another build of the same commit has a different image, for example after a CI re-run. Fix the cause with the steps below, then approve.
## Render in the same place
Screenshots render in your CI, so fonts and anti-aliasing depend on the machine. Take them in the same environment every time, for example the Playwright Docker image with the same Playwright version as your project. Do not mix screenshots from your laptop with screenshots from CI.
## Freeze the page
```ts tests/dashboard.spec.ts
import { test } from "@playwright/test" ;
import { snapshot } from "stateofpixel/playwright" ;
test ( "dashboard" , async ({ page }) => {
await page.clock. setFixedTime ( new Date ( "2026-01-01T10:00:00Z" ));
await page. goto ( "/dashboard" );
await page. getByRole ( "table" ). waitFor ();
await snapshot (page, "App/Dashboard" );
});
```
- `snapshot()` from the [Playwright integration](https://stateofpixel.com/docs/playwright.md) already waits for fonts, disables CSS animations and transitions, and hides the caret.
- Fix the clock with `page.clock` so dates and relative times do not change.
- Wait for the content you want, not for a fixed time.
- Replace random or live data, like avatars, ads and charts of today's numbers, with fixed test data, or hide it with CSS before the screenshot.
- For Storybook, use `--wait-for-selector` and `--delay` for stories that render late.
## Tune the threshold
Two project settings decide when pixels count as changed. Admins find them under Settings, Diff:
- **Threshold**, from 0 to 1, default 0.1. How different a pixel's color has to be to count. Higher ignores more.
- **Count anti-aliased pixels as changes**, off by default. Anti-aliased edges are ignored unless you turn it on.
`--threshold` on the CLI, or `threshold` on the Playwright reporter, overrides the setting for that upload. To try values before you change them, compare two folders on your machine:
```sh terminal
npx stateofpixel compare screenshots baseline --threshold 0.2
# Writes stateofpixel-report/index.html
```
---
# Troubleshooting
Most problems show up as a message from the CLI or a check description on GitHub. Find yours below.
## No check on the pull request
- **The pull request comes from a fork.** The CLI prints "skipped, GitHub Actions gives no OIDC token to pull requests from forks" and exits 0. GitHub gives fork pull requests no OIDC token and no secrets, so there is nothing to sign in with. See [Authentication](https://stateofpixel.com/docs/cli.md#authentication).
- **The workflow cannot get a token.** The CLI stops with "No token". Add `id-token: write` to the permissions of the job or the workflow.
- **stateofpixel was not reachable.** The CLI prints "skipped, the service is not reachable" after 3 retries and exits 0, so your CI keeps passing. Pass `--strict` to fail the job instead.
- **The OIDC token request failed.** The CLI retries a 5xx from GitHub and then skips like an outage. Any other status stops it with "GitHub Actions OIDC token request failed with HTTP 403" or similar.
- **The GitHub App does not have the repository.** The server answers `unauthorized`. Add the repository to the installation on GitHub.
## The CLI stops before uploading
- **"Not a directory".** The folder you passed does not exist. Check the path from the directory the command runs in.
- **"Set --nonce so every shard joins the same build."** Off GitHub Actions there is no run id to share, so sharded uploads need a nonce. Set `--nonce` or `STATEOFPIXEL_NONCE` to the same value in every shard. `finalize` asks for the same with "Set --nonce to the nonce the shards used." See [Other CI](https://stateofpixel.com/docs/other-ci.md).
- **"Invalid snapshot name".** A name passed to `snapshot()` has an empty part, like `Button//Primary`, or `..`. Name each part.
- **"No index.json".** `stateofpixel storybook` needs a built Storybook 7 or newer. Run `storybook build` first and pass its output folder.
- **"No stories found".** The Storybook has no stories, or `--include` and `--exclude` left none.
- **"Stories that did not render".** The listed stories failed to load, or did not match `--wait-for-selector`, within 15 seconds. Nothing was uploaded. See [Storybook](https://stateofpixel.com/docs/storybook.md).
- **"stateofpixel storybook needs Playwright".** Install it with `npm install -D playwright`, then `npx playwright install chromium`.
## The check says "Build never finished"
A build that is still waiting for screenshots 60 minutes after it started expires. This happens when a shard never uploaded, or when a build with `--shard auto` never ran `stateofpixel finalize`. A shard that arrives later is refused because the build "is already expired". On GitHub Actions, re-run the workflow: a new run attempt uses a new nonce and makes a new build. On other CI, use a new `STATEOFPIXEL_NONCE`. See [Sharding](https://stateofpixel.com/docs/sharding.md).
## The check stays at "Waiting for screenshots"
The build waits until every shard has uploaded. With `(2 of 4 shards)` in the description, the other shards have not reported yet. Every shard of one build needs the same nonce and the same shard total, or the server refuses it with `shard_total_mismatch`.
## Every snapshot is new, or the baseline is wrong
- **The default branch has no build yet.** Run the workflow on pushes to your default branch, so there is a baseline to compare with. See [Baselines](https://stateofpixel.com/docs/baselines.md).
- **The checkout is shallow.** The CLI sends up to 100 commits of history. With a shallow clone, the server asks GitHub instead and only checks the newest builds on the baseline branch. Use `fetch-depth: 0` in `actions/checkout`.
- **The suite name changed.** Baselines are kept per suite, so a new `--build-name` starts without one. See [Suites](https://stateofpixel.com/docs/suites.md).
## Snapshots failed
- **An image is too big.** Images over 20 MB (4 MB with [Netlify Blobs storage](https://stateofpixel.com/docs/limits.md#image-storage)) or 10,000 x 50,000 px are rejected, their snapshots fail and `upload` exits 1 with "uploads failed". Take a smaller screenshot or split a long page.
- **A file is not a PNG.** A file that ends in `.png` without being a PNG stops the upload with "Not a PNG file".
- **A name appears twice.** Two files with the same snapshot name in one build are refused with "appears more than once", or "was already sent by shard" across shards. Two Playwright projects with the same browser and width write the same file, so the second one wins. Give the snapshots different names.
## The same snapshot changes on every run
See [Stable screenshots](https://stateofpixel.com/docs/stable-screenshots.md). The snapshot detail says "Looks flaky" when stateofpixel has seen it change back and forth.
## Rate limits and storage
- **Too many requests.** The CLI prints "skipped, Too many requests for this token." or a daily limit message and exits 0. See [Limits and storage](https://stateofpixel.com/docs/limits.md#limits) for the numbers.
- **"Storage limit reached, not compared".** The account is past its storage limit and the grace period. New images are not stored, so changes are not compared. Upgrade, lower retention or delete a project. See [When an account is over its limit](https://stateofpixel.com/docs/limits.md#when-an-account-is-over-its-limit).
## Test the upload without sending anything
`npx stateofpixel upload screenshots --dry-run` hashes every image and prints the plan, the snapshot count and the number of ancestors it found, and uploads nothing.
---
# Accounts and projects
An account is the GitHub user or organization that installed the app. Each repository in the installation is a project.
## All projects
After you sign in, All projects lists the projects of every account you can see, grouped by account. Search filters them by name, and Overview opens the account page. An account shows "App not installed" when the GitHub App is not installed on it anymore. With no projects yet, the page has the button to install the app.
## The account page
The account page has five tabs:
- **Projects**: every repository in the installation, with its latest build. Search by name, and sort by project or by last update. Press `j` and `k` to move between rows in the project and build lists, and Enter to open one.
- **Members**: who has signed in to the account.
- **Usage**: storage per project, for owners only. See [The Usage tab](https://stateofpixel.com/docs/limits.md#the-usage-tab).
- **Billing**: the plan and its buttons. See [Billing](https://stateofpixel.com/docs/billing.md).
- **Settings**: where images are stored, for owners only. See [Image storage](https://stateofpixel.com/docs/limits.md#image-storage).
## Members
The Members tab lists everyone who has signed in to stateofpixel and has access to the account on GitHub, with their role and when they last signed in. Someone who never signed in is not listed.
Access comes from GitHub, so add or remove people there. In an organization, owners on GitHub are Owner and everyone else is Member. On a personal account, the account's user is Owner and everyone else is Collaborator. Owners change the plan and see usage. Repository permissions decide who can review builds and change project settings. See [Who can review](https://stateofpixel.com/docs/review.md#who-can-review).
## Project settings
Admins of the repository on GitHub open the project's Settings tab. Everyone else sees it disabled.
- **General**: the default branch, which mirrors GitHub, and the auto-approve branches. See [Baselines](https://stateofpixel.com/docs/baselines.md#the-default-branch).
- **Diff**: the threshold and whether anti-aliased pixels count. See [Tune the threshold](https://stateofpixel.com/docs/stable-screenshots.md#tune-the-threshold).
- **Checks**: the name of the check, `stateofpixel`. Other suites get their own check, like `stateofpixel/storybook`. See [Suites](https://stateofpixel.com/docs/suites.md).
- **Retention**: how long images kept only for pull requests stay. See [Retention](https://stateofpixel.com/docs/limits.md#retention).
- **Tokens**: project tokens for CI other than GitHub Actions. See [Other CI](https://stateofpixel.com/docs/other-ci.md).
- **Danger**: Delete project.
## Delete a project
Under Settings, Danger, type the repository name and press Delete project. Every build, review and token of the project is deleted right away, and this cannot be undone. Images that no other project uses are deleted by the next daily cleanup, which frees their storage.
CI uploads to the repository are refused after that. If the repository is still in the GitHub App installation, it comes back as an empty project after the next sync with GitHub, for example when you change the repositories of the installation. Remove the repository from the installation on GitHub to keep it out.
## Remove a repository or uninstall the app
Removing a repository from the installation on GitHub archives its project. Uninstalling or suspending the app archives every project of the account.
An archived project:
- does not show in the project list, and its pages say "Project not found."
- refuses uploads from CI, and gets no check on new commits
- keeps its builds, baselines and tokens, and its images still count toward storage. Retention still deletes old builds of pull requests and branches.
- still shows on the Usage tab while it holds images, marked "Archived"
Uninstalling does not cancel a paid plan. Cancel it under Billing first. See [Cancel](https://stateofpixel.com/docs/billing.md#cancel).
Add the repository again, or install the app again, and the project comes back with its builds, baselines and settings. To have an archived project's data deleted, write to us. See the [privacy policy](https://stateofpixel.com/privacy.md).
---
# Billing
Plans differ only in storage. Every plan has the same features, and builds, snapshots and reviewers are not charged.
## Plans
The Free plan has 10 GB of storage. Paid plans have 25 GB, 100 GB or 500 GB, billed monthly or yearly. See [pricing](https://stateofpixel.com/index.md#pricing) for the prices, and [Limits and storage](https://stateofpixel.com/docs/limits.md) for how storage is counted.
Only owners of the account on GitHub change the plan. Everyone else sees the buttons disabled, with a note on who can. See [Members](https://stateofpixel.com/docs/accounts.md#members).
## Upgrade
Open the account's Billing tab, press Upgrade and pick a plan and a billing period. Checkout opens with Dodo Payments, our payment provider, and brings you back to the Billing tab when it is done. The Billing tab then says "Payment received" while the plan updates, which takes a few seconds, "Your payment is processing" while the payment clears, or "The payment did not go through" when it failed and the plan did not change. The new storage limit applies as soon as the payment clears.
## Change plan
On a paid plan, Change plan moves to another size or billing period without cancelling first. Before anything changes, it shows what you pay now. Unused time on the current plan counts toward the new one, and anything left over is credited to later renewals. You cannot move to a plan smaller than what the account stores today, so free some space first.
## Payment method and invoices
Manage billing opens the Dodo Payments customer portal. Update the card and download invoices there.
## Cancel
Cancel from the customer portal under Manage billing. Cancel at the next billing date keeps the plan until the end of the billing period, and the Billing tab shows the day it ends. Then the account moves to the Free plan. Cancel now moves it to the Free plan right away. If it stores more than 10 GB by then, the 14 day grace period starts that day. See [When an account is over its limit](https://stateofpixel.com/docs/limits.md#when-an-account-is-over-its-limit).
Uninstalling the GitHub App does not cancel the plan. See [Remove a repository or uninstall the app](https://stateofpixel.com/docs/accounts.md#remove-a-repository-or-uninstall-the-app).
## When a payment fails
When a renewal fails, the account pages say that the last payment failed. Update the payment method under Manage billing to keep the plan.
See the [refund policy](https://stateofpixel.com/refunds.md) for refunds.
---
# Security
stateofpixel never receives your source code. It stores the screenshots your CI uploads, and access to them follows your permissions on GitHub.
## How CI signs in
On GitHub Actions the CLI asks GitHub for an OIDC token with the audience `stateofpixel`. The server checks its signature against GitHub's keys, its issuer and audience, and finds the project from the `repository_id` claim. The token is also tied to its commit: a build has to be for the commit of the run, or for the pull request the run belongs to. There is no secret to store or leak.
On other CI, the CLI sends a project token. Tokens start with `sop_` and are random. Only a SHA-256 hash of each token is stored, so a token is shown once when it is created and never again. Repository admins create and revoke tokens under Settings, Tokens, and see when each one was last used. See [Other CI](https://stateofpixel.com/docs/other-ci.md).
A CI token can only create builds for its own project. It cannot sign in to the site, review builds or change settings.
## Who can see and do what
Access comes from GitHub and there are no separate accounts or seats:
- **Read** access to the repository opens its builds and baselines.
- **Write** access approves and rejects changes.
- **Admin** access opens the project settings, where tokens, retention and project deletion live.
- **Owners** of the GitHub account or organization manage the plan and see usage.
Builds of public repositories can be opened by anyone who signs in with GitHub. Builds of private repositories show only to people who can read the repository on GitHub. See [how fast permission changes apply](https://stateofpixel.com/docs/review.md#who-can-review).
## Screenshots
Screenshots and diff images are stored in Convex file storage, or in Netlify Blobs when the account owner picks it. See [Image storage](https://stateofpixel.com/docs/limits.md#image-storage). Every image is stored once per account, keyed by its SHA-256 hash, and an upload is checked against that hash.
Image links of private projects are signed and expire after one to two hours. The site gets a new link only for someone who can read the repository. Image links of public projects do not expire.
Screenshots show whatever your pages render. Do not capture pages with data you are not allowed to share, such as real customer data.
## Webhooks
GitHub webhooks are checked against their `X-Hub-Signature-256` signature and refused when it does not match.
## Deleting data
- Builds of pull requests and other branches are deleted after the retention period in the project settings. See [Retention](https://stateofpixel.com/docs/limits.md#retention).
- Deleting a project in its settings deletes its builds, reviews and tokens right away. Images that no build uses any more are deleted by the next daily cleanup.
- Removing a repository from the GitHub App, or uninstalling it, archives the project. CI can no longer upload to it. Its data stays until you ask for it to be deleted.
The [privacy policy](https://stateofpixel.com/privacy.md) lists what is collected and which providers process it.
---
# CLI
The stateofpixel package on npm. Node 20 or newer. Run npx stateofpixel \ --help for the same list.
The source is in [`packages/cli`](https://github.com/VaibhavAcharya/stateofpixel/tree/main/packages/cli) of [VaibhavAcharya/stateofpixel](https://github.com/VaibhavAcharya/stateofpixel) under the MIT license. Open an [issue](https://github.com/VaibhavAcharya/stateofpixel/issues) for bugs and feature requests.
## Commands
| Command | What it does |
| ------------------------------ | ---------------------------------------------------------------- |
| `upload ` | Upload a folder of screenshots and compare it with the baseline. |
| `storybook ` | Capture every story of a built Storybook, then upload. |
| `finalize` | Finish a build whose shards ran with --shard auto. |
| `compare ` | Compare a folder of screenshots against a baseline folder. |
## upload
| Flag | Default | What it does |
| ----------------------------------------------------------- | ------- | --------------------------------------------------------------- |
| `--build-name ` STATEOFPIXEL\_BUILD\_NAME | | Separate builds of one project, like storybook. |
| `--shard ` STATEOFPIXEL\_SHARD | | This shard and the shard count, or auto with a finalize step. |
| `--nonce ` STATEOFPIXEL\_NONCE | | Shared by every shard of one build. |
| `--baseline-branch ` STATEOFPIXEL\_BASELINE\_BRANCH | | Branch to compare against. |
| `--subset` | | Only some snapshots ran, do not mark others removed. |
| `--threshold ` | | Color difference threshold, 0 to 1, overrides project settings. |
| `--strict` | | Fail instead of skipping on an outage, a rate limit or a fork. |
| `--dry-run` | | Hash and print the plan, upload nothing. |
## storybook
Takes every flag of `upload`, plus these. See [Storybook](https://stateofpixel.com/docs/storybook.md).
| Flag | Default | What it does |
| -------------------------------- | --------------------- | -------------------------------------- |
| `--viewports ` | `1280` | Comma separated viewport widths. |
| `--include ` | | Only stories whose title/name match. |
| `--exclude ` | | Skip stories whose title/name match. |
| `--wait-for-selector ` | `#storybook-root > *` | Wait for this before each screenshot. |
| `--delay ` | `0` | Wait this long before each screenshot. |
## finalize
Use the same values the shards used. See [Sharding](https://stateofpixel.com/docs/sharding.md).
| Flag | Default | What it does |
| ----------------------------------------------------------- | ------- | -------------------------------------------------------------- |
| `--build-name ` STATEOFPIXEL\_BUILD\_NAME | | The build name the shards used. |
| `--nonce ` STATEOFPIXEL\_NONCE | | The nonce the shards used. |
| `--baseline-branch ` STATEOFPIXEL\_BASELINE\_BRANCH | | Branch to compare against, for --skip-if-empty. |
| `--skip-if-empty` | | Create a build with no changes when no shard ran. |
| `--strict` | | Fail instead of skipping on an outage, a rate limit or a fork. |
## compare
| Flag | Default | What it does |
| ---------------------- | --------------------- | ------------------------------------- |
| `--out ` | `stateofpixel-report` | Report folder. |
| `--threshold ` | `0.1` | Color difference threshold, 0 to 1. |
| `--include-aa` | | Count anti-aliased pixels as changes. |
## Environment variables
The flags above list the variable that sets each of them. The CLI also reads these.
| Variable | What it does |
| -------------------- | ---------------------------------------------------------------------------------------------------------- |
| `STATEOFPIXEL_TOKEN` | Project token, for CI other than GitHub Actions. |
| `STATEOFPIXEL_DIR` | Folder that snapshot() writes to and the Playwright reporter uploads, stateofpixel-screenshots by default. |
| `CI` | The Playwright reporter uploads only when it is set. |
## Authentication
On GitHub Actions the CLI uses the OIDC token, which needs `permissions: id-token: write`. Anywhere else, set `STATEOFPIXEL_TOKEN` to a project token. See [Other CI](https://stateofpixel.com/docs/other-ci.md) and [Security](https://stateofpixel.com/docs/security.md#how-ci-signs-in).
GitHub gives pull requests from forks no OIDC token and no secrets, so the CLI skips them with a warning and sets no check. If `stateofpixel` is a required check, those pull requests cannot merge until an admin bypasses the rule. See [No check on the pull request](https://stateofpixel.com/docs/troubleshooting.md#no-check-on-the-pull-request).
## Output
```sh terminal
stateofpixel build #411 feat/header vs main (#405)
1,500 snapshots 1,488 unchanged 10 changed 2 added 1 removed
uploaded 22 images (1.3 MB) in 2.1 s
review: https://stateofpixel.com/acme/web-app/builds/411
```
## Exit codes
- **0** when the build was reported, with or without changes. The GitHub check decides whether the pull request can merge.
- **0** with a warning when stateofpixel or the GitHub OIDC token service is down, a rate limit is hit, or the pull request comes from a fork, so an outage does not break your CI. Pass `--strict` to exit 1 instead.
- **1** on configuration and authentication errors, failed uploads, and images over the [limits](https://stateofpixel.com/docs/limits.md). See [Troubleshooting](https://stateofpixel.com/docs/troubleshooting.md) for the common causes.
`compare` runs on your machine and exits 0 when images differ. Open its report to see the changes.
---
# Limits and storage
Plans are priced by storage only. Builds, snapshots and reviewers are not billed.
## How storage is counted
Every image is stored once per account, keyed by its content. A snapshot that did not change between builds adds nothing, however many builds use it. Storage is the size of every stored image: baselines, images from pull requests and diff images. The account's Billing tab shows how much is used. The Free plan has 10 GB, and [pricing](https://stateofpixel.com/index.md#pricing) lists the paid plans.
## Image storage
Images are stored in Convex file storage or in Netlify Blobs. Account owners pick one on the Settings tab of the account page. The choice applies to new uploads, and stored images stay where they are. Storage is counted the same way for both. Netlify Blobs takes images up to 4 MB, and Convex up to 20 MB.
## The Usage tab
Owners see a Usage tab on the account page. It splits storage into baselines, images kept only for pull requests and diff images, per project, and charts the last 90 days. It is counted once a day. An image used by several projects counts for the project that used it first.
## When an account is over its limit
1. At 80%, the account pages show a warning, and so do the project pages for people with write access. The CLI prints one too.
2. At 100%, a 14 day grace period starts. Everything keeps working, and the banner names the day it ends.
3. After the grace period, new images are not stored. Builds still report, but changed snapshots are not compared, and the check passes with "Storage limit reached, not compared". CI keeps passing.
Upgrading, or freeing space with a shorter retention or by deleting a project, ends this as soon as usage is back under the limit.
Owners of the account on GitHub change the plan. Everyone else in the account sees the same warnings, with a note to ask an owner.
A plan cancelled at the next billing date stays until the end of its billing period. If the account then stores more than the Free plan allows, the grace period starts that day. The banner warns about this as soon as the plan is cancelled, and 14 days before any cancelled plan ends.
## Retention
Builds of pull requests are deleted a set time after the pull request closes, 60 days by default. Builds of branches without a pull request are deleted when the branch had no new build for that long. Admins set it from 7 to 365 days under Settings, Retention, in Keep PR-only images for.
Builds on the default branch and on auto-approve branches are kept, and so is any build that another build uses as its baseline. A daily cleanup removes images that no build has used for 24 hours, and frees their storage. A link to a deleted build says when it was deleted and which rule deleted it.
## Limits
| Limit | Value |
| ---------------------- | ------------------------------ |
| Snapshots per build | 20,000 |
| Shards per build | 256 |
| Image size | 20 MB, 4 MB with Netlify Blobs |
| Image dimensions | 10,000 x 50,000 px |
| Snapshot name | 512 characters |
| Metadata per snapshot | 4 KB |
| Time to finish a build | 60 minutes |
| Builds per account | 2,000 a day |
| Uploads per account | 20 GB a day |
| Requests per token | 600 a minute |
Images over the size or dimension limits are rejected, their snapshots fail, and `upload` exits 1. Rate limits make the CLI warn and exit 0.