How to Test Responsive Navigation Menus with Website Screenshots
Test responsive navigation at real CSS breakpoints with consistent screenshots, then verify menu behavior and accessibility with Playwright.
Test responsive navigation by capturing its closed and open states at viewport widths just below and above the CSS breakpoints that change its layout. Compare those screenshots with reviewed baselines, then separately assert that the menu opens and closes, links work, and accessibility semantics are correct. Screenshots catch visible regressions; they do not prove behavior or accessibility.
This guide uses Playwright Test, which supports screenshot comparison and browser/device emulation. Use your project’s actual breakpoints: there is no universal width at which a navigation menu should become compact. See the Playwright screenshot testing guide and emulation documentation.
1. Map the menu states and breakpoint widths
Before writing tests, inspect the navigation’s CSS and interaction design. Record each breakpoint that changes the navigation, and decide which states are meaningful at each width.
| Layout | Widths to cover | States to capture |
|---|---|---|
| Desktop | A representative wide viewport and, if applicable, just below and above desktop layout breakpoints | Closed/default; open submenu or expanded state when supported |
| Compact/mobile | A representative narrow viewport and just below and above the compact-navigation breakpoint | Menu closed and open, plus an expanded submenu if relevant |
For a breakpoint at 768 CSS pixels, for example, consider 767 and 769 as boundary checks, along with a narrow phone width and a wide desktop width. Replace 768 with the breakpoint in your own stylesheet; the example is not a recommended universal breakpoint. If multiple breakpoints change the menu, cover each transition that could alter its layout or controls.
Use the actual menu button or other control to reach the open state. Do not fake the appearance with CSS or directly force an internal state: the test should exercise the visitor’s interaction path. Keep a small, intentional matrix first; expand it for supported browsers, high-use devices, and known layout risks.
2. Install Playwright Test
In a JavaScript or TypeScript project, install Playwright Test and its browser binaries using the commands from the official installation guide. A typical setup is:
npm init playwright@latest
The setup wizard creates a test configuration and sample tests. The examples below assume a local application is available at http://127.0.0.1:3000, the navigation has a button named “Menu,” and the links are inside a navigation landmark. Change those selectors and the app URL to match your application.
3. Write screenshot and behavior tests
This TypeScript example captures both menu states at widths around a 768-pixel breakpoint. On the first run, Playwright creates reference screenshots; review them before committing. Later runs compare new captures against those references.
import { test, expect } from '@playwright/test';
const widths = [
{ name: 'narrow', width: 375 },
{ name: 'just-below-breakpoint', width: 767 },
{ name: 'just-above-breakpoint', width: 769 },
{ name: 'wide', width: 1440 },
];
test.describe('responsive navigation', () => {
for (const viewport of widths) {
test(`${viewport.name}: closed and open menu`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: 900 });
await page.goto('http://127.0.0.1:3000');
const nav = page.getByRole('navigation', { name: 'Primary' });
const menuButton = page.getByRole('button', { name: 'Menu' });
// Capture the initial state after the page and navigation are ready.
await expect(nav).toBeVisible();
await expect(page).toHaveScreenshot(`nav-${viewport.name}-closed.png`, {
fullPage: false,
animations: 'disabled',
});
// On compact layouts the button opens the menu. On desktop, adapt this
// interaction to the site's actual submenu or expanded navigation control.
if (viewport.width <= 768) {
await menuButton.click();
await expect(menuButton).toHaveAttribute('aria-expanded', 'true');
await expect(nav.getByRole('link').first()).toBeVisible();
await expect(page).toHaveScreenshot(`nav-${viewport.name}-open.png`, {
fullPage: false,
animations: 'disabled',
});
await menuButton.click();
await expect(menuButton).toHaveAttribute('aria-expanded', 'false');
}
// Check structure and presence independently of the visual comparison.
await expect(nav.getByRole('link', { name: 'Home' })).toBeVisible();
});
}
});
Adjust the example to match the design. If desktop navigation has no open state, do not invent one; assert its default links and test any real desktop submenu control. If the compact menu is modal, check its dialog semantics and close control too. If links are hidden while closed, assert that behavior explicitly rather than relying on a screenshot alone.
Check keyboard operation and accessible semantics
Use a keyboard interaction test for the actual expected behavior. For example, if the menu button receives focus and opens with Enter or Space, test that path and verify focus behavior. Check the button’s accessible name, expanded state, and relationships such as aria-controls when your implementation uses them. Also verify that menu links have meaningful names and usable destinations. A screenshot cannot establish any of these facts.
test('compact menu is operable by keyboard', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('http://127.0.0.1:3000');
const button = page.getByRole('button', { name: 'Menu' });
await button.focus();
await expect(button).toBeFocused();
await page.keyboard.press('Enter');
await expect(button).toHaveAttribute('aria-expanded', 'true');
const nav = page.getByRole('navigation', { name: 'Primary' });
await expect(nav.getByRole('link', { name: 'Home' })).toBeVisible();
});
Playwright also supports accessibility snapshots for checking the accessibility tree. Use them where they fit your Playwright version and testing approach, alongside focused role, name, and state assertions. Text/value assertions and interaction checks cover page state that a visual image cannot.
4. Make screenshot comparisons repeatable
Visual baselines are only useful when the rendering conditions are controlled. Playwright notes that screenshots can vary with the host operating system, browser version, settings, hardware, power state, and headless mode. Run baseline creation and comparison in the same environment, and pin the browser versions used by your project.
- Set viewport width and height explicitly for every capture.
- Keep browser, operating system, device scale factor, fonts, and browser settings consistent between baseline and comparison runs.
- Choose viewport or full-page capture deliberately. Viewport capture represents the visible screen; full-page capture can reveal page-level effects caused by the menu.
- Keep pixel scale consistent. CSS-pixel captures are easier to compare across ordinary viewport tests; device-pixel captures are larger and depend on device scale factor.
- Disable animations for stable captures where motion is not under test. If animation itself matters, test it as behavior separately.
- Mask, hide, or normalize known volatile content such as timestamps or rotating promotions with a test stylesheet or screenshot options.
- Review baseline changes. Update references only after confirming the visual change is intended.
toHaveScreenshot waits for two consecutive screenshots to match before comparing, which helps avoid saving a frame while the page is still changing. It cannot make nondeterministic content stable by itself: wait for fonts and required content, control animation, and remove volatile regions where appropriate. See visual comparison options for threshold, pixel-difference, masking, and animation settings.
Choosing viewport, element, or full-page captures
| Capture | Use it for | Tradeoff |
|---|---|---|
| Viewport | The navigation as a visitor sees it at a given scroll position | Does not include content outside the viewport |
| Navigation element | Isolating menu alignment, spacing, clipping, and visible controls | Can miss overlap with the page or viewport edges |
| Full page | Page-wide shifts or content-flow consequences of an expanded menu | Captures more content and may introduce unrelated visual noise |
Playwright supports viewport, element, and full-page screenshots. For element capture, locate the navigation and call its screenshot assertion; retain a viewport capture too when overlap or positioning against surrounding content is important.
5. Handle baselines and tolerances carefully
Start with strict comparisons in a pinned environment. If genuine rendering variation remains, tune the threshold or maximum differing pixels using reviewed examples. There is no universal safe tolerance: a loose threshold can hide a broken icon, clipped link, or misplaced menu edge. Prefer masking a known volatile region over increasing tolerance for the entire image.
When a comparison fails, inspect the generated actual, expected, and diff images. Determine whether the change is a regression, an intentional design update, or environmental drift. Only then regenerate the expected screenshot and review the resulting file change.
6. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot differs on every run | Animation, blinking cursor, rotating content, delayed fonts, or unstable data | Wait for required content, disable nonessential animation, mask or normalize volatile regions, and keep the runtime environment fixed. |
| Menu-open screenshot looks closed | The click missed, the control is covered, or the test captured before the open state settled | Use a role-based locator, assert aria-expanded and visible links before capture, and investigate overlays or event failures. |
| Boundary tests show the same layout | The tested widths do not straddle the real CSS breakpoint, or another stylesheet/container rule controls the layout | Inspect the computed styles and actual media/container queries, then test immediately on both sides of the relevant transition. |
| Baseline differs on another machine | OS, browser build, fonts, scale factor, rendering mode, or hardware differs | Create and compare snapshots in a consistent environment; avoid accepting machine-specific diffs without review. |
| Links are visible in pixels but tests cannot find them | Missing navigation landmark, inaccessible name, or links are visually styled without semantic link elements | Fix semantic markup and accessible naming; use role-based locators and test the intended link structure. |
| Screenshot clips a dropdown or menu | The menu extends outside its scroll container or viewport, or the capture scope is too narrow | Check overflow and positioning; capture the viewport to expose clipping, and use an element/full-page capture only for the question it answers. |
| Test is flaky in parallel | Shared app state, test data, or mutable server content is affecting captures | Isolate test data and routes, avoid shared interactions, and stabilize content rather than broadly relaxing pixel comparisons. |
| Baseline update hides a regression | Expected images were regenerated before reviewing the difference | Review actual and diff images first; update baselines only for intentional UI changes. |
7. Run a practical test matrix
- Identify every CSS breakpoint that changes the navigation layout or control.
- Select widths just below and above each breakpoint, plus representative narrow and wide sizes.
- Capture the default state, the real open state, and meaningful submenu states at the widths where they exist.
- Run behavior checks for opening, closing, keyboard operation, focus, and link destinations.
- Run semantic/accessibility checks for names, roles, expanded state, and navigation structure.
- Review visual diffs and update only intentional baselines.
- Expand browser and device coverage according to supported platforms and known risk.
Playwright can emulate browser viewport and screen size, and offers device presets that include touch configuration. Emulation is useful for repeatable layout and interaction checks, but a small set of emulated profiles is not proof of behavior on every physical device. Add real-device testing where the supported product and risk require it.
Or skip the browser setup
For screenshot capture without managing browser setup, ScreenshotNeo takes a screenshot or PDF from one GET request. Its API supports viewport sizing, device presets, full-page captures, element selectors, custom CSS and JavaScript, waits, and other capture options; see the ScreenshotNeo API documentation. A screenshot API captures the page state you request: to test an interactive menu’s open state, you still need to trigger that state in a suitable way, such as with supported custom JavaScript or an application URL/state designed for testing.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Use your test page URL and API key in place of the example values. ScreenshotNeo removes cookie/consent banners, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.
Sign up for 1,000 free screenshots a month, with no card required.
Performance, reliability, and cost
Screenshot tests take longer than simple DOM assertions because they launch a browser, load a page, settle its state, and compare image data. Keep the matrix focused on breakpoint boundaries and representative sizes, and expand it where browser support or regression risk calls for more coverage. Run independent cases in parallel only when their app state and test data are isolated.
Reliable visual checks depend on deterministic content and a stable rendering environment. Treat reference images as reviewed test artifacts, keep browser versions aligned, and diagnose diffs before changing thresholds. For API-based captures, include request timeouts and handle non-success responses in the client. ScreenshotNeo bills only clean shots; its response indicates whether a result was billable, and cache hits are not billed.
For self-hosted Playwright, account for the engineering time and compute needed to maintain browser dependencies and run the chosen matrix. For ScreenshotNeo, the published plans are Free: 1,000 shots/month; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is available on every plan. Check the product site for current plan details.
Frequently asked questions
Should every menu state have a screenshot baseline?
Baseline each visually distinct state that matters to users, especially closed and open compact navigation. Include submenus when their layout or styling can regress.
Can screenshots verify that the menu is accessible?
No. They show rendered appearance. Use semantic, keyboard, focus, accessible-name, and link assertions to check operation and structure.
How many viewport widths should I test?
Cover both sides of every breakpoint that changes navigation, plus representative narrow and wide widths. Add widths for known device or layout risks rather than relying on a universal fixed list.
Should I use full-page screenshots?
Use them when the expanded menu affects page flow or content below the fold. Use viewport captures for the visitor’s current view, and element captures to inspect menu details.
When should I update a visual baseline?
After reviewing the diff and confirming the interface change is intended. A failed comparison alone is not a reason to replace the reference.


