Stop shipping UI bugs. Take screenshots in your Capybara tests, commit baselines to git, and let CI catch visual regressions in pull requests — no cloud service, no subscription, runs entirely in your test suite.
Why this gem? Baselines live in git — review UI changes in pull requests like you review code. Runs offline, works in CI, zero vendor lock-in. Unlike Percy/Chromatic (paid SaaS), nothing to sign up for. Unlike BackstopJS, no Node required.
Already using Capybara for system tests? Add the gem and you're ready. New to system tests? See Rails System Testing guide.
# Gemfile
gem 'capybara-screenshot-diff'
gem 'ruby-vips' # Optional: 10x faster comparisons# test/test_helper.rb
require 'capybara_screenshot_diff/minitest'# test/application_system_test_case.rb
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
include CapybaraScreenshotDiff::Minitest::Assertions
end# test/system/homepage_test.rb
class HomepageTest < ApplicationSystemTestCase
test "homepage" do
visit "/"
screenshot "homepage"
end
endThen run these steps in order:
# Step 1: Save baselines (first run always passes)
bundle exec rake test
# Step 2: Commit baselines to git
git add doc/screenshots/
git commit -m "chore: add screenshot baselines"
# Step 3: Now comparisons work — change your UI and re-run
bundle exec rake testAfter Step 1, you'll see:
doc/screenshots/
homepage.png <- your baseline (commit this)
Add diff artifacts to .gitignore — these are generated at runtime and should not be committed:
# Screenshot diff artifacts (generated, not committed)
*.diff.png
*.base.png
*.diff.webp
*.base.webp
snap_diff_report.htmlIf you skip Step 2 and push to CI, the build will fail — fail_if_new is true by default in CI.
For RSpec, Cucumber, or non-Rails setup, see Framework Setup.
require 'capybara_screenshot_diff/static'
CapybaraScreenshotDiff.serve("_site") # or "public", "build", "dist"Then commit baselines to git just like Rails. Full setup.
The test fails with a clear message and generates diff files:
Screenshot does not match for 'homepage':
({"area_size":1250,"region":[0,19,199,83],"max_color_distance":42.5})
Open doc/screenshots/homepage.diff.png to see exactly what changed. If the change is intentional, delete the baseline and re-run to update it.
| File | Description |
|---|---|
homepage.png |
Committed baseline |
homepage.diff.png |
Visual diff with changes highlighted in red |
homepage.heatmap.diff.png |
Heatmap of pixel differences |
Add one line to get an interactive dashboard for reviewing all screenshot differences:
# test/test_helper.rb
require 'capybara_screenshot_diff/reporters/html'After tests run, open doc/screenshots/snap_diff_report.html:
Review all visual changes in one place — no need to hunt through .diff.png files. 4 view modes (both/base/new/heatmap), per-image zoom, annotation toggle, keyboard navigation, and search.
In GitHub Actions, one step uploads the report, posts a PR comment with the link, and adds a job summary:
- name: Upload screenshot report
if: failure()
uses: snap-diff/snap_diff-capybara/.github/actions/upload-screenshots@master
with:
name: screenshots
pr-comment: 'true'See CI Integration for full setup including Ruby + libvips action and baseline update workflow.
Works without a browser — PDFs, generated images, CI artifacts:
result = Capybara::Screenshot::Diff.compare("baseline.png", "current.png")
result.different? # => true if visually different
result.quick_equal? # => true if byte-identical- Crop to element:
screenshot "form", crop: "#main-form" - Ignore regions:
screenshot "dashboard", skip_area: [".timestamp"] - Disable animations:
Capybara::Screenshot.disable_animations = true - Set window size:
Capybara::Screenshot.window_size = [1280, 1024]
Defaults work for most Rails apps — blur_active_element, hide_caret, and fail_if_new (in CI) are enabled automatically.
If screenshots differ between CI and local, set a comparison threshold:
Capybara::Screenshot::Diff.configure do |screenshot, diff|
screenshot.window_size = [1280, 1024] # consistent viewport
diff.perceptual_threshold = 2.0 # ignore anti-aliasing (VIPS only)
# or: diff.tolerance = 0.001 # percentage-based (default for VIPS)
endSee Choosing the Right Method for detailed comparison options.
The test passed on first run. Did it work?
Yes. First run saves baselines and always passes. Run tests again to compare against committed baselines.
How do I update baselines after intentional UI changes?
Delete the baseline file and re-run tests: rm doc/screenshots/homepage.png && bundle exec rake test. Or update all: rm -rf doc/screenshots/ && bundle exec rake test.
CSS animations make my screenshots flaky
Enable Capybara::Screenshot.disable_animations = true to freeze CSS animations/transitions before each capture. Or use stability_time_limit: 1 to wait for animations to finish.
CI screenshots differ from local
Set window_size for consistent dimensions and use perceptual_threshold: 2.0 to ignore anti-aliasing differences across environments.
Will this slow down my tests?
Comparisons add ~50ms per image with VIPS. Without ruby-vips, ChunkyPNG is used (slower but no system dependency). stability_time_limit adds wait time — keep it low (0.1-0.5s) or use disable_animations instead.
Debug mode
DEBUG=1 bundle exec rake test keeps .diff.png files for inspection.
Requirements: Ruby 3.2+. Rails 7.1+ for Rails integration; non-Rails projects supported via CapybaraScreenshotDiff.serve(). For the :vips driver: libvips 8.9+. On macOS: brew install vips. On Ubuntu: apt-get install libvips-dev.
- Framework Setup — Minitest, RSpec, Cucumber
- CI & Non-Rails Integration — GitHub Actions, reusable action, static sites, baseline updates
- Configuration Reference — all options explained
- Image Processing Drivers — VIPS, ChunkyPNG, perceptual threshold
- Screenshot Organization — groups, sections, cropping, multi-browser
- Web UI & Custom Reporters — interactive report, custom reporters
After checking out the repo, run bin/setup then rake test. See Docker Testing for reproducible CI-matching test runs.
See CONTRIBUTING.md
The gem is available as open source under the terms of the MIT License.

