Audit Accessibility and Visual Regressions Before Launch
Run axe, walk the keyboard path, test reflow and reduced motion, and compare screenshots before and after a change. After this, you ship with a checklist and not a hope.
Course content
Sign in to save your progress.
-
Architecture That Scales
-
Modern CSS Without a Framework Crutch
-
Cascade Layers and Specificity You Control
-
-
-
Layout Systems for Real Sites
-
Complex Grids and Nested Layout Patterns
-
Container Queries When the Component Is the Breakpoint
-
-
-
Motion and Interaction Done Right
-
Transitions and Animations That Don't Annoy People
-
prefers-reduced-motion and Respecting the User
-
-
-
Performance and Delivery
-
Critical CSS and What Blocks First Paint
-
Font Loading Without Layout Jump
-
Responsive Images: srcset, sizes and Lazy Load
-
-
-
Ship and Maintain
-
Design Tokens and Theming Without Duplicating Everything
-
CSS That Survives a Redesign
-
Audit Accessibility and Visual Regressions Before Launch
-
-
- Final Check: HTML and CSS Advanced
Skip ahead: If you already run axe, tab through the page, and keep screenshot baselines that fail a build, skip to "Visual regressions", then continue.
Most accessibility and layout bugs ship because nobody looked at the finished page in the way a user does. Looking takes an hour once and ten minutes after that. Skipping it costs support tickets and fixes that touch every page. Do three passes: automated checks for what a tool can see, manual checks for what it cannot, and screenshots to catch what you changed by accident.
Pass 1: automated, and what it misses
I ran axe-core 4.14.0 in Chrome 154 against a small contact page with a light grey intro paragraph, a button and a link. This is the part of the report that matters:
color-contrast serious .muted
Element has insufficient color contrast of 2.64 (foreground color: #9aa0a6,
background color: #ffffff, font size: 12.0pt (16px), font weight: normal).
Expected contrast ratio of 4.5:1
landmark-one-main moderate html Document does not have a main landmark
region moderate 4 nodes Some page content is not contained by landmarks
Three findings, and each one is real. The grey text fails the 4.5:1 ratio for normal text. The page has no <main>, so keyboard and screen reader users cannot jump to the content. #626976 on white computes to 5.52:1, which passes, and that was a one-line fix.
The same page had a:focus, .btn:focus { outline: none; } in its CSS. axe reported nothing about that. After two Tab presses the focused element was the button, and its outline-style was none. A tool cannot tell that a focus indicator is missing, because that depends on what you can see. That is the pass you do by hand.
Pass 2: manual
- Tab through the whole page. Can you see where you are at every step? Does the order match the visual order? Can you reach and use every control without a mouse?
- Zoom to 200%, then narrow the window to 320 CSS pixels. WCAG 1.4.10 asks for content to reflow at that width without two-direction scrolling.
- Emulate reduced motion. Nothing should slide or loop, and nothing important should vanish.
- Check contrast for every state, not just the default: hover, focus, disabled, error, and each theme.
Non-text parts need 3:1 as well, such as input borders and focus rings (WCAG 1.4.11). In my demo the input border #c9ced6 on white is 1.58:1, so a field drawn only by that border is hard to see.
⚠ Warning: A green axe report does not mean the page is accessible. Automated tools catch only part of the problems. Treat a clean report as the start of the audit and not the end.
Visual regressions
Screenshots catch the change you did not mean to make. Take a baseline of the page at two widths, make a change, take it again, and compare. This is the script I used, with Playwright 1.63.0 and Pillow 12.3.0.
from playwright.sync_api import sync_playwright
url, out = "http://127.0.0.1:8765/page.html", "current"
with sync_playwright() as p:
browser = p.chromium.launch()
for width in (1280, 390):
page = browser.new_page(viewport={"width": width, "height": 800})
page.goto(url)
page.screenshot(path=f"{out}-{width}.png", full_page=True)
browser.close()
from PIL import Image, ImageChops
for width in (1280, 390):
a = Image.open(f"baseline-{width}.png").convert("RGB")
b = Image.open(f"current-{width}.png").convert("RGB")
box = ImageChops.difference(a, b).getbbox()
print(f"{width}px:", "identical" if box is None else f"pixels differ inside {box}")
Two runs with no change printed identical at both widths. After I changed the .btn padding from 8px 16px to 8px 20px on a page with three buttons, both widths printed pixels differ inside (25, 8, 72, 101). The box tells you where to look. Pixel diffs are strict: fonts, anti-aliasing and animations create noise, so pin the browser version, disable animations, and wait for fonts before capture.
Common mistakes
- Running the audit on the staging page with sample content, and then launching with real content that is longer.
- Fixing contrast only in light mode.
- Treating every pixel difference as a failure. Decide what is worth a human look.
- Auditing once, at launch, and never again.
💡 Tip: Put the axe run and the screenshot compare in the build, so a regression fails before a reviewer sees it. Keep the manual pass as a short checklist in the pull request template.
What happens if you skip this
The first person to find the missing focus ring is a user who cannot use the form. The first person to find the broken layout is a customer on a narrow phone. Both reports arrive after launch.
Try it
Run axe on your most important page, then Tab through it with your mouse unplugged. Write down every place where you could not see the focus.
How many of the problems you found would a tool have reported?
About
Run axe, walk the keyboard path, test reflow and reduced motion, and compare screenshots before and after a change. After this, you ship with a checklist and not a hope.
- Duration
- 5 min
- Last update
- 08/07/2026
Comments (0)
Sign in to commentNo comments yet.