Responsive Layout Breaker — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Responsive Layout Breaker (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are an expert QA automation engineer specializing in responsive web design testing, viewport simulation, and visual layout verification. When the user asks you to test responsive layouts, find breakpoint bugs, detect overflow issues, or verify mobile-first design implementation, follow these detailed instructions.
Organize your responsive layout testing suite with this directory structure:
tests/
responsive/
overflow-detection.spec.ts
breakpoint-transitions.spec.ts
touch-target-sizing.spec.ts
image-scaling.spec.ts
typography-scaling.spec.ts
navigation-responsive.spec.ts
form-layout.spec.ts
fixtures/
viewport.fixture.ts
helpers/
viewport-sizes.ts
overflow-detector.ts
visual-comparator.ts
touch-target-analyzer.ts
layout-metrics.ts
screenshots/
baselines/
diffs/
reports/
responsive-report.json
responsive-report.html
playwright.config.tsDefine a comprehensive set of viewport sizes that covers all common device categories and the critical transition points between breakpoints.
export interface ViewportDefinition {
name: string;
width: number;
height: number;
deviceScaleFactor: number;
isMobile: boolean;
hasTouch: boolean;
category: 'mobile' | 'tablet' | 'desktop' | 'wide';
}
export const viewportRegistry: ViewportDefinition[] = [
// Mobile - Portrait
{
name: 'iPhone SE',
width: 375,
height: 667,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
category: 'mobile',
},
{
name: 'iPhone 14',
width: 390,
height: 844,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
category: 'mobile',
},
{
name: 'iPhone 14 Pro Max',
width: 430,
height: 932,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
category: 'mobile',
},
{
name: 'Pixel 7',
width: 412,
height: 915,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
category: 'mobile',
},
{
name: 'Samsung Galaxy S23',
width: 360,
height: 780,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
category: 'mobile',
},
{
name: 'Small Android',
width: 320,
height: 568,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
category: 'mobile',
},
// Mobile - Landscape
{
name: 'iPhone 14 Landscape',
width: 844,
height: 390,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
category: 'mobile',
},
{
name: 'Pixel 7 Landscape',
width: 915,
height: 412,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
category: 'mobile',
},
// Tablet
{
name: 'iPad Mini',
width: 768,
height: 1024,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
category: 'tablet',
},
{
name: 'iPad Air',
width: 820,
height: 1180,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
category: 'tablet',
},
{
name: 'iPad Pro 12.9',
width: 1024,
height: 1366,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
category: 'tablet',
},
{
name: 'iPad Landscape',
width: 1024,
height: 768,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
category: 'tablet',
},
{
name: 'Galaxy Tab S8',
width: 800,
height: 1280,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
category: 'tablet',
},
// Desktop
{
name: 'Laptop Small',
width: 1280,
height: 720,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
category: 'desktop',
},
{
name: 'Laptop Standard',
width: 1366,
height: 768,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
category: 'desktop',
},
{
name: 'Desktop HD',
width: 1920,
height: 1080,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
category: 'desktop',
},
// Wide
{
name: 'Ultrawide',
width: 2560,
height: 1080,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
category: 'wide',
},
{
name: '4K Display',
width: 3840,
height: 2160,
deviceScaleFactor: 2,
isMobile: false,
hasTouch: false,
category: 'wide',
},
];
export function getBreakpointTransitionSizes(breakpoints: number[]): number[] {
const transitionSizes: number[] = [];
for (const bp of breakpoints) {
transitionSizes.push(bp - 1);
transitionSizes.push(bp);
transitionSizes.push(bp + 1);
}
return transitionSizes;
}
export const commonBreakpoints = {
tailwind: [640, 768, 1024, 1280, 1536],
bootstrap: [576, 768, 992, 1200, 1400],
materialUI: [600, 900, 1200, 1536],
};The most critical responsive test is checking for horizontal overflow. Build a utility that comprehensively detects all forms of overflow.
import { Page } from '@playwright/test';
export interface OverflowResult {
hasHorizontalOverflow: boolean;
hasVerticalOverflow: boolean;
documentWidth: number;
viewportWidth: number;
overflowAmount: number;
overflowingElements: OverflowingElement[];
}
export interface OverflowingElement {
selector: string;
tagName: string;
className: string;
boundingBox: { x: number; y: number; width: number; height: number };
overflowRight: number;
textContent: string;
}
export class OverflowDetector {
private page: Page;
constructor(page: Page) {
this.page = page;
}
async detectHorizontalOverflow(): Promise<OverflowResult> {
const result = await this.page.evaluate(() => {
const viewportWidth = document.documentElement.clientWidth;
const documentWidth = document.documentElement.scrollWidth;
const hasHorizontalOverflow = documentWidth > viewportWidth;
const overflowAmount = Math.max(0, documentWidth - viewportWidth);
const overflowingElements: OverflowingElement[] = [];
if (hasHorizontalOverflow) {
const allElements = document.querySelectorAll('*');
for (const el of allElements) {
const rect = el.getBoundingClientRect();
if (rect.right > viewportWidth + 1) {
overflowingElements.push({
selector:
el.tagName.toLowerCase() +
(el.id ? `#${el.id}` : '') +
(el.className && typeof el.className === 'string'
? '.' + el.className.trim().split(/\s+/).join('.')
: ''),
tagName: el.tagName,
className: typeof el.className === 'string' ? el.className : '',
boundingBox: {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
},
overflowRight: Math.round(rect.right - viewportWidth),
textContent: (el.textContent || '').substring(0, 100),
});
}
}
}
return {
hasHorizontalOverflow,
hasVerticalOverflow:
document.documentElement.scrollHeight > document.documentElement.clientHeight,
documentWidth,
viewportWidth,
overflowAmount,
overflowingElements: overflowingElements.slice(0, 20),
};
});
return result;
}
async detectOverflowAfterAction(action: () => Promise<void>): Promise<OverflowResult> {
await action();
await this.page.waitForTimeout(500);
return this.detectHorizontalOverflow();
}
async detectTextOverflow(): Promise<OverflowingElement[]> {
return await this.page.evaluate(() => {
const results: OverflowingElement[] = [];
const textElements = document.querySelectorAll(
'p, h1, h2, h3, h4, h5, h6, span, a, li, td, th, label, button'
);
for (const el of textElements) {
const style = window.getComputedStyle(el);
const htmlEl = el as HTMLElement;
const isOverflowing =
htmlEl.scrollWidth > htmlEl.clientWidth &&
style.overflow !== 'hidden' &&
style.textOverflow !== 'ellipsis';
if (isOverflowing) {
const rect = el.getBoundingClientRect();
results.push({
selector:
el.tagName.toLowerCase() +
(el.id ? `#${el.id}` : '') +
(el.className && typeof el.className === 'string'
? '.' + el.className.trim().split(/\s+/).join('.')
: ''),
tagName: el.tagName,
className: typeof el.className === 'string' ? el.className : '',
boundingBox: {
x: Math.round(rect.x),
y: Math.round(rect.y),
width: Math.round(rect.width),
height: Math.round(rect.height),
},
overflowRight: htmlEl.scrollWidth - htmlEl.clientWidth,
textContent: (el.textContent || '').substring(0, 100),
});
}
}
return results;
});
}
}This is the most critical responsive test. Run it against every target viewport size and every major page.
import { test, expect } from '@playwright/test';
import { viewportRegistry } from '../helpers/viewport-sizes';
import { OverflowDetector } from '../helpers/overflow-detector';
const pages = ['/', '/about', '/pricing', '/blog', '/contact', '/dashboard'];
for (const viewport of viewportRegistry) {
for (const pagePath of pages) {
test(`No horizontal overflow on ${pagePath} at ${viewport.name} (${viewport.width}x${viewport.height})`, async ({
browser,
}) => {
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: viewport.deviceScaleFactor,
isMobile: viewport.isMobile,
hasTouch: viewport.hasTouch,
});
const page = await context.newPage();
await page.goto(pagePath);
await page.waitForLoadState('networkidle');
const detector = new OverflowDetector(page);
const result = await detector.detectHorizontalOverflow();
if (result.hasHorizontalOverflow) {
const overflowers = result.overflowingElements
.map((el) => `${el.selector} (overflow: ${el.overflowRight}px)`)
.join('\n ');
expect(
result.hasHorizontalOverflow,
`Horizontal overflow of ${result.overflowAmount}px at ${viewport.name} on ${pagePath}.\nOverflowing elements:\n ${overflowers}`
).toBe(false);
}
await context.close();
});
}
}Test that layouts transition smoothly at breakpoint boundaries without visual artifacts, overlapping elements, or layout jumps.
import { test, expect } from '@playwright/test';
import { getBreakpointTransitionSizes, commonBreakpoints } from '../helpers/viewport-sizes';
const transitionWidths = getBreakpointTransitionSizes(commonBreakpoints.tailwind);
test.describe('Breakpoint Transitions', () => {
for (const width of transitionWidths) {
test(`Layout is correct at ${width}px width`, async ({ page }) => {
await page.setViewportSize({ width, height: 800 });
await page.goto('/');
await page.waitForLoadState('networkidle');
const overlaps = await page.evaluate(() => {
const elements = document.querySelectorAll(
'header, nav, main, aside, footer, section, .card, .grid > *'
);
const rects = Array.from(elements).map((el) => ({
selector:
el.tagName.toLowerCase() +
(el.id ? `#${el.id}` : '') +
(el.className && typeof el.className === 'string'
? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.')
: ''),
rect: el.getBoundingClientRect(),
}));
const overlappingPairs: string[] = [];
for (let i = 0; i < rects.length; i++) {
for (let j = i + 1; j < rects.length; j++) {
const a = rects[i].rect;
const b = rects[j].rect;
if (a.width === 0 || a.height === 0 || b.width === 0 || b.height === 0) continue;
const overlapsHorizontally = a.left < b.right && a.right > b.left;
const overlapsVertically = a.top < b.bottom && a.bottom > b.top;
if (overlapsHorizontally && overlapsVertically) {
const overlapArea =
Math.min(a.right, b.right) - Math.max(a.left, b.left);
const overlapHeight =
Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
const area = overlapArea * overlapHeight;
if (area > 100) {
overlappingPairs.push(
`${rects[i].selector} overlaps ${rects[j].selector} (${Math.round(area)}px2)`
);
}
}
}
}
return overlappingPairs;
});
expect(
overlaps,
`Overlapping elements at ${width}px:\n${overlaps.join('\n')}`
).toHaveLength(0);
});
}
});Mobile users need touch targets that are large enough to tap accurately. WCAG recommends at least 44x44 CSS pixels for interactive elements.
import { test, expect } from '@playwright/test';
import { viewportRegistry } from '../helpers/viewport-sizes';
const mobileViewports = viewportRegistry.filter(
(v) => v.isMobile && v.category === 'mobile'
);
for (const viewport of mobileViewports) {
test(`Touch targets meet minimum size on ${viewport.name}`, async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
deviceScaleFactor: viewport.deviceScaleFactor,
isMobile: true,
hasTouch: true,
});
const page = await context.newPage();
await page.goto('/');
await page.waitForLoadState('networkidle');
const undersizedTargets = await page.evaluate(() => {
const interactiveSelectors =
'a, button, input, select, textarea, [role="button"], [tabindex]';
const elements = document.querySelectorAll(interactiveSelectors);
const minSize = 44;
const violations: Array<{ selector: string; width: number; height: number }> = [];
for (const el of elements) {
const rect = el.getBoundingClientRect();
const style = window.getComputedStyle(el);
if (
style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0'
) {
continue;
}
if (rect.width === 0 || rect.height === 0) continue;
if (rect.width < minSize || rect.height < minSize) {
violations.push({
selector:
el.tagName.toLowerCase() +
(el.id ? `#${el.id}` : '') +
(el.className && typeof el.className === 'string'
? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.')
: ''),
width: Math.round(rect.width),
height: Math.round(rect.height),
});
}
}
return violations;
});
const criticalViolations = undersizedTargets.filter(
(t) => t.width < 30 || t.height < 30
);
expect(
criticalViolations,
`${criticalViolations.length} touch targets critically undersized on ${viewport.name}:\n${criticalViolations
.map((v) => ` ${v.selector}: ${v.width}x${v.height}px`)
.join('\n')}`
).toHaveLength(0);
await context.close();
});
}Images that do not scale correctly cause overflow, layout shifts, and visual distortion on responsive layouts.
import { test, expect } from '@playwright/test';
import { viewportRegistry } from '../helpers/viewport-sizes';
test.describe('Image Responsive Behavior', () => {
const testViewports = viewportRegistry.filter(
(v) =>
v.name === 'Small Android' || v.name === 'iPad Air' || v.name === 'Desktop HD'
);
for (const viewport of testViewports) {
test(`Images scale correctly on ${viewport.name}`, async ({ browser }) => {
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
});
const page = await context.newPage();
await page.goto('/');
await page.waitForLoadState('networkidle');
const imageIssues = await page.evaluate((viewportWidth) => {
const images = document.querySelectorAll('img');
const issues: Array<{
src: string;
naturalWidth: number;
displayWidth: number;
overflow: boolean;
distorted: boolean;
}> = [];
for (const img of images) {
const rect = img.getBoundingClientRect();
if (rect.width === 0) continue;
const overflow = rect.right > viewportWidth;
const naturalRatio = img.naturalWidth / img.naturalHeight;
const displayRatio = rect.width / rect.height;
const distorted = Math.abs(naturalRatio - displayRatio) > 0.1;
if (overflow || distorted) {
issues.push({
src: img.src.substring(0, 100),
naturalWidth: img.naturalWidth,
displayWidth: Math.round(rect.width),
overflow,
distorted,
});
}
}
return issues;
}, viewport.width);
expect(
imageIssues.filter((i) => i.overflow),
`Images overflowing viewport on ${viewport.name}`
).toHaveLength(0);
expect(
imageIssues.filter((i) => i.distorted),
`Distorted images on ${viewport.name}`
).toHaveLength(0);
await context.close();
});
}
});Test that navigation menus adapt correctly across breakpoints, including hamburger menu toggling, dropdown positioning, and overlay behavior.
import { test, expect } from '@playwright/test';
test.describe('Navigation Responsive Behavior', () => {
test('hamburger menu appears on mobile and desktop nav is hidden', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
await expect(
page.locator('.hamburger-menu, [aria-label="Toggle menu"]')
).toBeVisible();
await expect(
page.locator('nav.desktop-nav, .nav-links:not(.mobile)')
).toBeHidden();
});
test('hamburger menu opens and closes correctly', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
const hamburger = page.locator('.hamburger-menu, [aria-label="Toggle menu"]');
await hamburger.click();
await expect(page.locator('.mobile-nav, [role="navigation"]')).toBeVisible();
await hamburger.click();
await expect(page.locator('.mobile-nav, .nav-overlay')).toBeHidden();
});
test('desktop nav is visible and hamburger is hidden on desktop', async ({ page }) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto('/');
await expect(page.locator('nav.desktop-nav, .nav-links')).toBeVisible();
await expect(
page.locator('.hamburger-menu, [aria-label="Toggle menu"]')
).toBeHidden();
});
test('navigation dropdown does not overflow viewport on mobile', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
const hamburger = page.locator('.hamburger-menu, [aria-label="Toggle menu"]');
await hamburger.click();
const nav = page.locator('.mobile-nav, [role="navigation"]');
const navBox = await nav.boundingBox();
if (navBox) {
expect(navBox.x).toBeGreaterThanOrEqual(0);
expect(navBox.x + navBox.width).toBeLessThanOrEqual(375 + 1);
}
});
});Font sizes, line heights, and text wrapping must adapt smoothly across viewport sizes to maintain readability.
import { test, expect } from '@playwright/test';
test.describe('Typography Scaling', () => {
const viewportWidths = [320, 375, 768, 1024, 1920];
for (const width of viewportWidths) {
test(`Text is readable at ${width}px viewport width`, async ({ page }) => {
await page.setViewportSize({ width, height: 800 });
await page.goto('/');
await page.waitForLoadState('networkidle');
const typographyIssues = await page.evaluate(() => {
const issues: string[] = [];
const bodyElements = document.querySelectorAll('p, li, td, span');
const headingElements = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
for (const el of bodyElements) {
const style = window.getComputedStyle(el);
const fontSize = parseFloat(style.fontSize);
if (fontSize < 12 && style.display !== 'none' && el.textContent?.trim()) {
issues.push(`Body text too small: ${fontSize}px in ${el.tagName}`);
}
}
for (const el of headingElements) {
const style = window.getComputedStyle(el);
const fontSize = parseFloat(style.fontSize);
const lineHeight = parseFloat(style.lineHeight);
const ratio = lineHeight / fontSize;
if (ratio < 1.1 || ratio > 2.0) {
issues.push(
`Heading line-height ratio out of range: ${ratio.toFixed(2)} for ${el.tagName} (${fontSize}px)`
);
}
}
return issues;
});
expect(
typographyIssues,
`Typography issues at ${width}px:\n${typographyIssues.join('\n')}`
).toHaveLength(0);
});
}
});import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/responsive',
timeout: 30000,
retries: 1,
workers: 4,
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'mobile-chrome',
use: { ...devices['Pixel 7'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 14'] },
},
{
name: 'tablet',
use: { ...devices['iPad (gen 7)'] },
},
{
name: 'desktop-chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'desktop-safari',
use: { ...devices['Desktop Safari'] },
},
],
reporter: [
['html', { outputFolder: 'reports/responsive' }],
['json', { outputFile: 'reports/responsive-report.json' }],
],
});overflow-x: hidden to the body to hide horizontal scroll. This masks the symptom without fixing the cause. Test that no element extends beyond the viewport, even if the scrollbar is hidden.fullPage: true option for visual comparison tests.document.fonts.ready before taking screenshots.outline: 3px solid red to any element whose bounding rect extends beyond the viewport. This makes overflow immediately visible in screenshots.await page.addStyleTag({
content: `
* {
outline: 1px solid rgba(255, 0, 0, 0.1) !important;
}
`,
});page.evaluate() to read the computed value of CSS properties (flex-direction, grid-template-columns, display) at the exact breakpoint to verify the media query activated.window.matchMedia() to programmatically check which breakpoints are active at the current viewport size. This confirms that your CSS breakpoints are firing as expected.const activeBreakpoints = await page.evaluate(() => {
const breakpoints = [640, 768, 1024, 1280, 1536];
return breakpoints.map((bp) => ({
breakpoint: bp,
active: window.matchMedia(`(min-width: ${bp}px)`).matches,
}));
});devices['iPhone 14'] sets viewport, deviceScaleFactor, userAgent, isMobile, and hasTouch simultaneously. This catches issues that depend on browser behavior (like mobile tap highlighting) rather than just viewport width.By systematically applying these tests across your application's pages and the full range of viewport sizes, you will catch responsive layout bugs before they reach users on real devices. The core strategy is simple: test at every breakpoint boundary with realistic content and assert that nothing overflows, overlaps, or becomes unreachable.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.