ScreenshotNeo

BlogHow-to

How to Test a Website’s Responsive Navigation Menu with Screenshot Tests

Test responsive navigation with Playwright screenshots at your real breakpoints, compare reviewed baselines, and assert that menu controls work.

By the ScreenshotNeo team4 October 20268 min read

Test a responsive navigation menu by capturing its closed and open states at viewport widths around the breakpoints your CSS actually uses, then comparing each screenshot with a reviewed baseline. Add functional assertions for opening, closing, and keyboard behavior: a screenshot can reveal visual regressions, but cannot prove the control works.

This guide uses Playwright Test with TypeScript. Replace the example path, selectors, accessible names, and viewport dimensions with values from your application.

1. Choose viewports from your breakpoints

There is no universal mobile-navigation breakpoint. Find the media queries or container rules that change your navigation, then test immediately below and above each relevant transition. Include one representative narrow viewport and one wide viewport if they exercise distinct layouts. Test height too when it changes the menu’s scroll area, overlay, or positioning.

For example, if the navigation switches layout at 768 CSS pixels, useful boundary checks might use widths of 767 and 768. These are examples only: use your own breakpoint and verify whether the transition occurs at that exact width based on your CSS.

Axis What to cover
Viewport Widths on both sides of each navigation breakpoint; add representative compact and wide sizes.
State Closed, open, and any visually distinct submenu or overlay states that matter.
Scope Whole page for layout shifts and overlap; navigation element for a focused comparison.
Environment Keep browser version, operating system, fonts, and screenshot settings consistent with the approved baseline.

2. Set up a Playwright visual test

Install Playwright Test using your project’s existing package manager and browser setup. The following is a complete test file once you substitute your app URL and selectors. It captures the page before and after opening the menu at three illustrative widths.

import { test, expect } from '@playwright/test';

const cases = [
  { name: 'compact', width: 375, height: 812 },
  { name: 'breakpoint-below', width: 767, height: 900 },
  { name: 'breakpoint-at', width: 768, height: 900 },
  { name: 'wide', width: 1280, height: 800 },
];

test.describe('responsive navigation visual states', () => {
  for (const viewport of cases) {
    test(`${viewport.name}: closed and open`, async ({ page }) => {
      // Set the viewport before navigation so the page initializes at this size.
      await page.setViewportSize({ width: viewport.width, height: viewport.height });
      await page.goto('/');

      const menuButton = page.getByRole('button', { name: /menu/i });
      const navigation = page.getByRole('navigation', { name: /main/i });

      // Wait for the app's real readiness signal if it has one. Avoid arbitrary
      // sleeps unless a known animation or delayed UI requires a bounded wait.
      await expect(menuButton).toBeVisible();
      await page.mouse.move(-1, -1); // Avoid a hover style affecting the baseline.

      await expect(page).toHaveScreenshot(`navigation-${viewport.name}-closed.png`);
      await menuButton.click();
      await expect(menuButton).toHaveAttribute('aria-expanded', 'true');
      await expect(navigation).toBeVisible();
      await expect(page).toHaveScreenshot(`navigation-${viewport.name}-open.png`);
    });
  }
});

Set baseURL in your Playwright configuration or use an absolute URL in page.goto(). Use the accessible name your control actually exposes. If the desktop navigation has no menu button, make separate desktop assertions and snapshots for its links instead of forcing the mobile interaction pattern into that case.

3. Generate and review baselines

On the first run, Playwright Test creates reference screenshots; later runs compare current captures with those references. Review the generated files as expected UI states, and commit them with the test when appropriate for your repository. Treat a proposed baseline change as a design change: inspect why it changed and confirm the navigation still meets the intended behavior before accepting it. See the Playwright visual comparisons guide.

Run the visual test in the same environment used to establish its baseline. Browser version, host operating system, rendering settings, hardware, power source, and headless mode can affect pixels. A baseline from one environment may differ from a run in another even when the application code is unchanged.

4. Assert behavior separately from appearance

Visual snapshots do not establish that a menu button responds, that it closes, or that its expanded state is announced. Add assertions for the contract your component promises. For a button with an aria-expanded attribute, for example:

test('menu opens and closes', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 812 });
  await page.goto('/');

  const button = page.getByRole('button', { name: /menu/i });
  const navigation = page.getByRole('navigation', { name: /main/i });

  await expect(button).toHaveAttribute('aria-expanded', 'false');
  await button.click();
  await expect(button).toHaveAttribute('aria-expanded', 'true');
  await expect(navigation).toBeVisible();

  await button.click();
  await expect(button).toHaveAttribute('aria-expanded', 'false');
  await expect(navigation).toBeHidden();
});

If the design supports Escape to close, focus movement, or keyboard activation, write explicit assertions for those behaviors too. Use role and label locators where possible; they make the test reflect what assistive technology users can find. For a component whose accessible contract differs, assert that actual contract instead of copying these example expectations.

5. Capture the right scope

Use full-page screenshots when you need to catch navigation-induced shifts, clipped content, or overlap with the page. If unrelated page content makes the comparison noisy, assert a screenshot of the menu element itself:

await expect(page.getByRole('navigation', { name: /main/i}))
  .toHaveScreenshot('main-navigation-open.png');

An element capture focuses review on the navigation, but may miss a menu that covers or pushes the wrong content. Keep at least one page-level check where those surrounding effects matter. Playwright documents page capture and viewport sizing in its Page API.

6. Stabilize captures without hiding real regressions

  • Control hover: Move the pointer away before capture if hover styles alter the menu. If hover is itself a required state, test and name it as a separate state.
  • Wait for application readiness: Prefer a visible, stable UI signal or the application’s normal readiness condition. A fixed delay is slower and may still be too short on a slow run.
  • Handle animation deliberately: If a transition makes captures inconsistent, use Playwright’s screenshot styling options to disable the relevant animation for the visual assertion, or wait for the transition to finish. Keep a separate interaction test if the animation or transition behavior itself matters.
  • Suppress only known noise: Screenshot styling can hide or mask known dynamic elements. Do not hide the navigation or its contents to make a failing comparison pass.
  • Tune tolerance after inspection: Playwright supports pixel threshold and maxDiffPixels controls. Start with defaults, inspect actual diffs, and adjust only for understood rendering noise. A generous tolerance can conceal real layout regressions.
  • Keep content deterministic: Use stable test data, avoid changing timestamps or randomized content, and control network-dependent content when it appears in the capture.

See Playwright’s visual comparison documentation for screenshot comparison options and techniques to neutralize hover effects or apply styles during capture.

7. Common failures and fixes

Symptom Likely cause What to do
Snapshots differ on every run Uncontrolled dynamic content, hover, animation, font loading, or an unstable environment. Wait on a meaningful readiness signal, move the pointer away, stabilize known dynamic elements, and run with the same browser and host setup used for the baseline.
The mobile layout is missing in the capture The viewport was changed after navigation, or the wrong viewport was set. Set the viewport before goto(), as shown. Playwright notes that many sites do not expect phone-sized pages to be resized after loading.
Menu locator times out The accessible name or role differs, the control is hidden at that width, or the page is not ready. Inspect the rendered accessibility tree and use the control’s real role and name. Use a separate desktop test if the button is only present in the compact layout.
Open-state screenshot matches the closed state The click did not open the menu, the wrong control was selected, or the screenshot was taken before the state settled. Assert the expanded state and menu visibility before capturing. Fix the behavior or wait for the appropriate UI signal.
Large diffs after a browser or CI change Different browser version, OS, fonts, rendering settings, or headless mode. Restore the approved baseline environment or intentionally review and regenerate baselines in the new environment.
Too many harmless pixel diffs Rendering noise or unstable content is being compared too strictly. Identify and stabilize the source first. Then consider a narrowly scoped threshold or differing-pixel limit, reviewing that it still catches meaningful changes.
Visual test passes but keyboard use is broken The test checks pixels only. Add interaction assertions for keyboard operation, focus, closing behavior, and accessible state as required by the component.

8. Performance, reliability, and cost

Every viewport and state adds a browser capture and comparison, so choose cases that cover real layout transitions and user-visible states. Avoid repeating identical widths without a reason. Element screenshots can make diffs easier to review, while page screenshots cover surrounding layout effects. Reusing the same browser and test setup as the baseline reduces avoidable discrepancies. Keep retries and tolerance policies from masking consistent failures; investigate a failure before updating its reference.

Playwright’s comparison is local to your test workflow: the work is browser execution plus storing and reviewing reference images. The dossier does not establish a universal runtime, infrastructure price, or screenshot-test cost, so measure these in your own CI environment and account for the browser workers and artifact storage your project uses.

Or skip the browser setup

If you need a captured page without managing browser automation, ScreenshotNeo is a website screenshot API and MCP server. A screenshot API capture is useful for inspecting a rendered page, but it does not replace Playwright’s repeatable interactions, assertions, and reviewed test baselines for responsive-menu regression testing.

One GET request returns an image or PDF. The examples use the API shown in the ScreenshotNeo documentation; replace the target URL with a page you can access and put your key in the request.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.write(r.content)

Node.js

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}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots.

Sign up free for 1,000 screenshots a month, with no card required.

FAQ

Should I snapshot every possible viewport width?

No. Cover the actual breakpoints and representative layouts, then add a width when it exercises a distinct behavior or has caught a real class of defect.

Can a screenshot test replace accessibility checks?

No. Pair it with assertions for the control’s accessible role and state, plus keyboard and focus behavior that your menu supports.

Should I accept a new baseline when CI reports a diff?

Only after reviewing the difference and confirming it is the intended design. A generated image is a proposed reference, not an automatic approval.