QA How-To
How to Test responsive layouts in Cypress (2026)
Learn cypress how to test responsive layouts with viewport matrices, breakpoint assertions, overflow checks, orientation coverage, and CI-friendly patterns.
22 min read | 2,651 words
TL;DR
Use `cy.viewport(width, height)` for each risk band, then assert what the layout must do at that size: correct nav tree, usable primary actions, and no harmful overflow. Expand only critical journeys across sizes and keep visual diffs as a second layer.
Key Takeaways
- Drive sizes with `cy.viewport()` or config defaults and keep size intent inside the test.
- Assert layout contracts: mounted trees, visibility, CTA reachability, and overflow.
- Test both sides of each CSS breakpoint that changes navigation or page structure.
- Prefer relative geometry and overflow checks over brittle absolute pixel coordinates.
- Use a small risk-based size matrix instead of multiplying the full suite by every device.
- Add visual snapshots only for stable, high-risk templates with proper masking.
- Name failures with viewport labels so CI triage is immediate.
The practical answer to cypress how to test responsive layouts is: set the viewport for each risk band, visit the real page, assert layout-critical visibility and geometry, and only then decide whether visual snapshots or multi-browser runs add signal. Cypress exposes cy.viewport() and Cypress.config('viewportWidth' | 'viewportHeight'), so you can drive phone, tablet, and desktop dimensions without inventing a responsive API.
Responsive bugs rarely look like classic assertion failures. A filter drawer may cover the submit button, a sticky header may hide the first form field, a table may overflow on a 360px screen, or a desktop-only sidebar may remain visible after a breakpoint change. This guide shows how to design viewport matrices, write layout assertions that survive minor CSS tweaks, cover orientation and dynamic height, and keep responsive coverage honest in CI.
TL;DR
| Goal | Cypress approach | What to assert |
|---|---|---|
| Smoke a mobile path | cy.viewport(390, 844) then flow |
Primary CTA visible and clickable |
| Prove a breakpoint hide | Set width below and above the CSS breakpoint | Element exists or does not exist as designed |
| Catch overflow | Measure scrollWidth vs clientWidth |
No horizontal overflow on the page root |
| Cover orientation | Swap width and height | Layout still usable after rotate |
| Visual regression | Snapshot after layout settles | Diff only stable regions |
| CI matrix | Spec-level viewports or config overrides | Unique failure messages per size |
Responsive testing is not "run every test at three sizes." It is risk-based coverage of layout contracts that change with width, height, and device chrome.
1. Cypress How to Test Responsive Layouts: Define the Contracts
Before writing assertions, name the layout contracts that matter. A product may claim that below 768px the navigation collapses into a menu button, that the checkout CTA stays above the fold on common phones, that data tables switch to stacked cards under 640px, and that sticky footers never cover inputs. Those are testable contracts. Pixel-perfect sameness across browsers is usually not the contract.
Map contracts to evidence:
- Presence and absence: desktop sidebar removed on mobile, hamburger shown only under a breakpoint.
- Interaction: mobile menu opens, focus traps if required, overlay dismisses.
- Geometry: primary action is in the viewport, no element covers another critical control.
- Overflow: document does not force horizontal scroll for expected content widths.
- Content reflow: multi-column grids become single column without clipping text.
Cypress is excellent at the first four when you combine viewport control with DOM and geometry checks. Pure visual tools help with spacing drift and unintended z-index changes, but they are a second layer. Start with behavioral contracts so a CSS token tweak does not create dozens of noisy snapshot diffs.
Also document which sizes you care about. "Mobile" is not one device. A 320px older phone, a 390px modern phone, a 768px tablet, and a 1280px laptop exercise different CSS media queries. Pick sizes that match your CSS breakpoints and your analytics, not an arbitrary fashion list.
2. Control Viewport Size With cy.viewport()
cy.viewport() sets the width and height for subsequent commands in the current test. You can pass numbers or a preset name:
it('shows the mobile navigation control at phone width', () => {
cy.viewport(390, 844)
cy.visit('/dashboard')
cy.get('[data-cy=nav-menu-button]').should('be.visible')
cy.get('[data-cy=desktop-sidebar]').should('not.exist')
})
Preset names such as iphone-x, ipad-2, and macbook-15 are convenient for readability. Prefer explicit widths when your CSS uses a specific breakpoint like min-width: 1024px, because a preset may not land exactly on that boundary.
You can also set defaults in configuration:
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
},
})
Default config size is useful for desktop-first suites. For responsive cases, still set the viewport inside the test or in a beforeEach so the intent is local and reviewable. A global mobile default that surprises desktop specs creates flaky, confusing failures.
cy.viewport() changes the browser viewport used by Cypress. It does not emulate full device chrome, OS fonts, notch safe areas, or mobile browser URL bars with perfect fidelity. Treat it as a strong layout-width tool, not a complete device lab replacement.
3. Build a Practical Viewport Matrix
A good matrix is small and intentional:
| Label | Width | Height | Why it exists |
|---|---|---|---|
| phone-sm | 360 | 740 | Common Android width near lower CSS band |
| phone | 390 | 844 | Primary mobile journey |
| tablet | 768 | 1024 | Exact or near common breakpoint |
| desktop | 1280 | 800 | Primary desktop layout |
| wide | 1440 | 900 | Optional marketing and dense tables |
Encode the matrix once and reuse it:
export const viewports = {
phoneSm: { w: 360, h: 740, label: 'phone-sm' },
phone: { w: 390, h: 844, label: 'phone' },
tablet: { w: 768, h: 1024, label: 'tablet' },
desktop: { w: 1280, h: 800, label: 'desktop' },
} as const
export function setViewport(
key: keyof typeof viewports,
) {
const { w, h } = viewports[key]
cy.viewport(w, h)
}
Do not expand every end-to-end journey across every size. Expand journeys that change by breakpoint: navigation, forms with sticky actions, data tables, checkout, and marketing heroes. Leave pure API-backed logic tests at one size.
When looping sizes, keep failure identity clear:
const sizes = [
{ w: 360, h: 740, name: 'phone-sm' },
{ w: 390, h: 844, name: 'phone' },
{ w: 1280, h: 800, name: 'desktop' },
] as const
sizes.forEach(({ w, h, name }) => {
it(`keeps checkout CTA usable on ${name}`, () => {
cy.viewport(w, h)
cy.visit('/checkout')
cy.get('[data-cy=pay-now]')
.should('be.visible')
.and(($el) => {
const rect = $el[0].getBoundingClientRect()
expect(rect.bottom, `${name} CTA bottom`).to.be.lessThan(
Cypress.config('viewportHeight') + 1,
)
})
})
})
The size name in the assertion message saves triage time when only one band fails.
4. Assert Visibility, Existence, and Breakpoint Behavior
Many responsive bugs are simple: the wrong component tree for the width. Prefer existence checks when the desktop tree is unmounted, and visibility checks when it is only hidden:
it('switches from sidebar to drawer below the desktop breakpoint', () => {
cy.viewport(1280, 800)
cy.visit('/projects')
cy.get('[data-cy=desktop-sidebar]').should('be.visible')
cy.get('[data-cy=mobile-nav-button]').should('not.exist')
cy.viewport(390, 844)
cy.get('[data-cy=desktop-sidebar]').should('not.exist')
cy.get('[data-cy=mobile-nav-button]').should('be.visible').click()
cy.get('[data-cy=mobile-drawer]').should('be.visible')
cy.contains('[data-cy=mobile-drawer] a', 'Settings').click()
cy.location('pathname').should('eq', '/settings')
})
Test both sides of the breakpoint. A component that is correctly hidden at 390px but still present when it should appear at 1280px is half-tested. If CSS uses display: none rather than conditional render, assert not.be.visible instead of not.exist. Match the product's real DOM strategy.
Also verify that hidden desktop links are not the only path to critical pages. Mobile users must complete the same authorization and navigation outcomes through the mobile tree.
5. Catch Overflow, Clipping, and Covered Controls
Horizontal overflow is a classic mobile defect. You can assert page-level overflow without a visual tool:
function assertNoHorizontalOverflow(label: string) {
cy.document().then((doc) => {
const root = doc.documentElement
expect(
root.scrollWidth,
`${label} horizontal overflow`,
).to.be.at.most(root.clientWidth + 1)
})
}
it('does not force horizontal scroll on the pricing page at 360px', () => {
cy.viewport(360, 740)
cy.visit('/pricing')
assertNoHorizontalOverflow('pricing phone-sm')
cy.contains('h1', 'Pricing').should('be.visible')
})
For covered controls, compare bounding rectangles. A sticky footer that overlaps the final form field is a high-severity UX bug:
it('keeps the email field free of sticky footer overlap on phone', () => {
cy.viewport(390, 844)
cy.visit('/signup')
cy.get('[data-cy=email]').then(($field) => {
cy.get('[data-cy=sticky-cta-bar]').then(($bar) => {
const field = $field[0].getBoundingClientRect()
const bar = $bar[0].getBoundingClientRect()
const overlaps =
field.bottom > bar.top &&
field.top < bar.bottom &&
field.right > bar.left &&
field.left < bar.right
expect(overlaps, 'email overlapped by sticky bar').to.eq(false)
})
})
})
Geometry assertions are brittle if they hard-code exact pixel coordinates. Prefer relative relationships, viewport inclusion, and overflow checks. Avoid asserting exact top values unless the design system documents a hard offset.
6. Test Orientation Changes and Dynamic Viewport Height
Many CSS bugs appear only after rotation. Swap dimensions after an initial visit:
it('keeps media controls usable after landscape rotation', () => {
cy.viewport(390, 844)
cy.visit('/player/demo')
cy.get('[data-cy=play]').should('be.visible').click()
cy.viewport(844, 390)
cy.get('[data-cy=play]').should('be.visible')
cy.get('[data-cy=timeline]').should('be.visible')
assertNoHorizontalOverflow('player landscape')
})
Dynamic toolbars on mobile browsers change the visual viewport. Cypress viewport height is still a fixed test control, so you cannot fully reproduce every browser chrome animation. You can still test product behavior that depends on 100dvh vs 100vh by asserting that bottom-anchored actions remain interactable at your chosen height, and by including a slightly shorter height in the matrix when analytics show common short viewports.
If the app listens to resize or orientationchange, trigger the viewport change after hydration and wait for a stable UI signal (menu closed, skeleton gone) before asserting. Do not assume layout is complete at the moment cy.viewport() returns.
7. Responsive Images, Tables, and Forms
Responsive layouts fail in content-heavy regions as often as in chrome.
Images and media: assert that hero images scale within the container and that srcset or art-direction swaps are acceptable when the product depends on them. Prefer checking natural layout outcomes over parsing every candidate URL unless CDN behavior is the risk.
Tables: wide tables may scroll inside a container, collapse into cards, or hide lower-priority columns. Test the designed strategy explicitly:
it('uses stacked invoice cards on phone instead of a wide table', () => {
cy.viewport(390, 844)
cy.visit('/invoices')
cy.get('[data-cy=invoice-table]').should('not.exist')
cy.get('[data-cy=invoice-card]').should('have.length.at.least', 1)
cy.get('[data-cy=invoice-card]').first().click()
cy.location('pathname').should('match', /\/invoices\//)
})
Forms: multi-column forms should reflow without clipping labels or pushing errors off-screen. Assert label association remains intact and that validation messages are visible after submit on the small viewport.
For related network-aware UI timing while layouts load data, see how to wait for an API response in Cypress. Layout and data readiness are separate failure modes and should not be conflated.
8. When to Add Visual Regression for Responsive Layouts
Behavioral checks catch missing menus and covered buttons. Visual checks catch spacing collapse, incorrect font scaling, and accidental color or shadow regressions. Use visual tools when design risk is high and the page has stable regions.
Practical rules:
- Wait for fonts, images, and skeleton dismissal before snapshot.
- Mask dynamic clocks, avatars, and ads.
- Snapshot per named viewport, not after every minor interaction.
- Prefer component or page-level snapshots over whole-app tours.
- Treat diffs as design review signals, not automatic product bugs.
Example pattern with a placeholder snapshot command your project already uses:
it('matches the phone marketing hero layout', () => {
cy.viewport(390, 844)
cy.visit('/')
cy.get('[data-cy=hero]').should('be.visible')
cy.get('[data-cy=hero-image]').should('be.visible')
// Project-specific visual helper, for example cy.compareSnapshot
cy.get('[data-cy=hero]').matchImageSnapshot('home-hero-phone')
})
Use the actual visual plugin API your repository standardizes on. Do not invent a global matchImageSnapshot if your stack uses another helper. If you need broader visual strategy notes, compare approaches in Cypress visual testing.
9. Cypress How to Test Responsive Layouts in CI
CI should run the same viewport contracts as local development. Pin viewport sizes in the test, not only in a developer's laptop window. Headless Chrome in CI is usually enough for layout width and DOM structure. Add WebKit or Firefox only when browser engine differences have bitten the product.
Parallelization tips:
- Keep responsive specs deterministic; avoid depending on window resize from previous tests.
- Reset viewport in
beforeEachwhen a file mixes desktop and mobile cases. - Name tests with size labels for clearer CI logs.
- Do not multiply a 40-minute suite by five sizes. Extract a short responsive smoke suite and a deeper matrix for critical pages.
Container width must match expectations. If Cypress runs inside a nested browser environment with constrained dimensions, explicit cy.viewport() still sets the application viewport used for layout, but always verify a failing size locally before blaming CI infrastructure.
For pipeline wiring patterns, GitHub Actions for Playwright is a different runner, yet the matrix idea transfers: size and browser dimensions should be first-class job inputs when they affect risk. Cypress teams can mirror that thinking with focused responsive jobs rather than one opaque mega-job.
10. Design a Layered Responsive Strategy
A durable strategy looks like this:
| Layer | Owns | Avoid using it for |
|---|---|---|
| Unit or component tests | Breakpoint class logic, menu state | Full-page overflow |
| Cypress E2E | Navigation contracts, form usability, overflow, CTA reachability | Every spacing token |
| Visual snapshots | Design drift on key templates | Highly dynamic dashboards without masks |
| Real device lab | Touch, OS browsers, notches, performance | Replacing cheap viewport smoke tests |
Write Cypress tests that fail with a clear product meaning: "mobile nav missing at 390," "CTA covered by sticky bar," "pricing overflows at 360." Those messages beat a generic "element not visible" after a mystery resize.
Selectors should be stable across layouts. Prefer data-cy attributes shared by mobile and desktop variants, or explicit mobile and desktop test ids when the trees differ. Avoid CSS class soup that changes with design refactors. The Cypress data-cy selectors guide covers durable locator strategy.
Interview Questions and Answers
Q: How do you test responsive layouts in Cypress?
I set explicit viewports that match product breakpoints, then assert layout contracts: which navigation tree is mounted, whether primary actions are visible and unobstructed, and whether the page overflows horizontally. I expand only journeys that change by size.
Q: Is cy.viewport('iphone-x') enough?
It is a readable preset, but if CSS breakpoints use exact widths, I prefer numeric sizes that sit clearly below and above those breakpoints. Presets are fine when they still exercise the intended media queries.
Q: How do you avoid running the entire suite on every device size?
I maintain a small responsive smoke set for critical paths and leave most functional tests at one default desktop or mobile size. Risk, not fashion, drives the matrix.
Q: How can you detect horizontal overflow?
Compare documentElement.scrollWidth to clientWidth after the page has settled. I also inspect known wide components like tables when overflow is intentionally contained inside a child scroller.
Q: What is the difference between not.exist and not.be.visible for responsive UI?
not.exist fits conditional rendering where the desktop tree is unmounted. not.be.visible fits CSS hiding. I match the assertion to the implementation so failures stay accurate.
Q: When do visual snapshots help responsive testing?
When spacing, alignment, or layering can regress without breaking flow assertions. I snapshot stable regions after load completion and mask dynamic content.
Q: Does Cypress fully emulate mobile devices?
No. It controls viewport dimensions and runs real browser engines, but it does not perfectly reproduce every mobile browser chrome, network profile, or touch nuance. Combine Cypress with device labs when those risks matter.
Common Mistakes
- Running every end-to-end test at five sizes without increasing product signal.
- Using only desktop defaults and never asserting mobile navigation.
- Checking a single side of a breakpoint.
- Hard-coding exact x/y pixel coordinates that break on harmless spacing tweaks.
- Ignoring horizontal overflow and sticky overlap bugs.
- Treating visual diffs as automatic failures without masks or stable wait conditions.
- Relying on class names that change between mobile and desktop skins.
- Forgetting to reset viewport between mixed-size tests in one file.
- Assuming
cy.viewport()proves real-device touch and performance characteristics. - Multiplying CI time with redundant browser and size matrices.
Conclusion
For cypress how to test responsive layouts, set intentional viewports, assert the layout contracts that actually change with width and height, and keep deep visual or multi-browser coverage reserved for high-risk templates. cy.viewport() plus existence, visibility, overflow, and geometry checks will catch most ship-blocking responsive defects.
Start with one revenue path on phone and desktop sizes. Prove navigation, primary CTA reachability, and no page-level horizontal overflow. Expand the matrix only where analytics or past incidents show breakpoint risk, and keep each failure message tied to a named size and contract.
11. Stable Selectors and Page Objects for Multi-Layout UIs
Responsive products often mount different component trees. If mobile and desktop share the same business outcome, prefer shared semantic hooks:
// Prefer one stable hook when both trees expose the same control
cy.get('[data-cy=create-project]').click()
When the controls truly differ, name them honestly:
cy.get('[data-cy=create-project-mobile]').click()
// or
cy.get('[data-cy=create-project-desktop]').click()
Page objects should accept the active layout rather than hiding viewport changes inside random helpers:
class ProjectsPage {
openCreate(layout: 'mobile' | 'desktop') {
if (layout === 'mobile') {
cy.get('[data-cy=nav-menu-button]').click()
cy.get('[data-cy=create-project-mobile]').click()
} else {
cy.get('[data-cy=create-project-desktop]').click()
}
}
}
Avoid dual page objects that drift. One module with explicit layout branches is easier to review than two nearly identical classes. Never encode CSS breakpoints inside selectors such as .hidden.md:flex; those classes are implementation details and change during design refactors.
Also test keyboard and focus where responsive UI introduces overlays. After opening a mobile drawer, assert focus moves into the dialog when accessibility requires it, and assert that Esc or the close control restores a usable page. Layout quality includes operability, not only pixel arrangement.
12. Debugging Responsive Failures Quickly
When a responsive test fails, separate three causes: wrong viewport intent, timing before layout settlement, and a real product defect.
- Log or assert the active size at the start of the test with
Cypress.config('viewportWidth'). - Capture a screenshot on failure; many CI setups already do this.
- Check whether the assertion used
existvsvisibleincorrectly. - Confirm the app finished route transitions and data loading.
- Reproduce with the same numeric viewport locally before changing product code.
A frequent false alarm is asserting desktop-only text that is still in the DOM but visually replaced. Another is clicking a desktop control that is hidden but still matches a loose selector. Strict selectors and layout-aware existence checks prevent both.
If only one size in a loop fails, keep the loop. Do not delete the matrix and "fix" the suite by testing only desktop. The single-size failure is often the only signal that a media query regression reached main.
For flaky resize behavior, ensure you are not chaining rapid viewport changes without waiting for a UI steady state. Applications that recalculate positions on resize may need one assertion on a settled element before geometric checks.
13. Cypress How to Test Responsive Layouts: Example End-to-End Suite Slice
The following suite slice shows a realistic shape for a product shell. Adapt routes and selectors to your app; do not copy business IDs blindly.
describe('App shell responsiveness', () => {
const phone = { w: 390, h: 844 }
const desktop = { w: 1280, h: 800 }
it('completes project creation on phone', () => {
cy.viewport(phone.w, phone.h)
cy.visit('/projects')
cy.get('[data-cy=nav-menu-button]').click()
cy.get('[data-cy=create-project]').click()
cy.get('[data-cy=project-name]').type('Mobile Launch')
cy.get('[data-cy=save-project]').should('be.visible').click()
cy.contains('[data-cy=toast]', 'Project created').should('be.visible')
cy.document().its('documentElement').should(($root) => {
expect($root[0].scrollWidth).to.be.at.most($root[0].clientWidth + 1)
})
})
it('completes project creation on desktop', () => {
cy.viewport(desktop.w, desktop.h)
cy.visit('/projects')
cy.get('[data-cy=desktop-sidebar]').should('be.visible')
cy.get('[data-cy=create-project]').click()
cy.get('[data-cy=project-name]').type('Desktop Launch')
cy.get('[data-cy=save-project]').click()
cy.contains('[data-cy=toast]', 'Project created').should('be.visible')
})
it('hides dense table columns at tablet width if designed to', () => {
cy.viewport(768, 1024)
cy.visit('/projects')
cy.get('[data-cy=projects-table]').should('be.visible')
cy.get('[data-cy=col-owner]').should('be.visible')
cy.get('[data-cy=col-updated-long]').should('not.exist')
})
})
This style keeps each test readable, states the viewport up front, and asserts outcomes users care about. It is enough to block many regressions without a device farm.
Pair it with a short checklist in pull requests that touch global layout, navigation, or shared form shells: which breakpoints changed, which contracts were updated, and whether screenshots were reviewed intentionally.
Interview Questions and Answers
How would you validate a responsive website using Cypress?
I define the layout contracts per breakpoint, set matching viewports with `cy.viewport()`, and assert navigation structure, primary action usability, and overflow. I keep a small matrix for critical paths and use visual checks only where spacing risk is high.
How do you choose viewport sizes for automation?
I align sizes to product CSS breakpoints and real traffic bands, then place tests clearly below and above each important threshold. I avoid huge device lists that do not change assertions.
How do you test a hamburger menu in Cypress?
I set a mobile viewport, assert the menu button is visible and the desktop nav is absent or hidden, open the drawer, navigate to a critical link, and confirm the route. I also verify the desktop side of the breakpoint.
What assertions catch elements covered by sticky footers?
I compare bounding client rectangles of the field and the sticky bar and fail when they intersect while both are relevant to the user task. Relative geometry is more stable than hard-coded coordinates.
How do responsive tests differ from cross-browser tests?
Responsive tests change layout dimensions and breakpoint behavior. Cross-browser tests change engines. They overlap but answer different risks, so I do not treat one matrix as a substitute for the other.
How would you keep responsive Cypress tests maintainable?
I centralize named viewports, use stable `data-cy` selectors, assert contracts instead of pixels, limit size multiplication, and put clear size labels in test titles and assertion messages.
What would you do if a responsive test fails only in CI?
I confirm the test sets an explicit viewport, compare screenshots or DOM dumps, check for race conditions before layout settlement, and verify the app URL and data seed match local runs.
Frequently Asked Questions
How do I set a mobile screen size in Cypress?
Call `cy.viewport(390, 844)` or another width and height before interacting with the page. You can also pass a preset name such as `iphone-x` when that size still hits your CSS breakpoints.
Can Cypress test responsive CSS media queries?
Yes. Changing the viewport width and height causes the application to re-evaluate media queries. Assert the DOM and layout outcomes those queries are supposed to produce.
How do I test both mobile and desktop navigation in one suite?
Write focused tests that set an explicit viewport, assert the expected navigation tree, and exercise the path to a critical page. Reset the viewport when a file mixes multiple sizes.
How can I detect horizontal scrolling bugs with Cypress?
Compare `document.documentElement.scrollWidth` with `clientWidth` after the page settles. Fail when scrollWidth is greater than the client width for pages that must not overflow.
Should every Cypress test run on multiple viewports?
No. Multiply sizes only for journeys and pages whose layout contracts change by breakpoint. Keep most functional tests at one default size to control runtime and noise.
Does cy.viewport emulate a real phone?
It sets the browser viewport dimensions used for layout, which is enough for many responsive checks. It does not fully replace real-device testing for touch, OS browsers, notches, or mobile performance.
When should I add visual regression to responsive tests?
Add it when design drift can ship without breaking flow assertions. Snapshot stable regions per named viewport after load completion, and mask dynamic content.
Related Guides
- How to Debug a failing test in VS Code in Cypress (2026)
- How to Test responsive layouts in Playwright (2026)
- How to Test responsive layouts in Selenium (2026)
- How to Debug a failing test in VS Code in Playwright (2026)
- How to Debug a failing test in VS Code in Selenium (2026)
- How to Run a single test in Cypress (2026)