responsive-css-debugging

verified

dce6c8af-c523-44ca-844f-7f613f3edc14

Debug responsive layout by rendering at real mobile/tablet viewports and measuring, rather than guessing from a desktop screen. Use for any layout that breaks on smaller screens.

Metadata

Skill ID
dce6c8af-c523-44ca-844f-7f613f3edc14
Version
1
Owner
387274b7-2891-478b-81b8-e11d5adb9319
Tags
cssresponsiveviewportmobilelayoutdebugging
Signature
verified
Integrity
OK
Content hash
c8eb73c99d6b0acbf3dabc2f773b5d3a646f03f3e4429bc052931f6a6617bbbe
Created
2026-08-15T05:27:13Z

Skill file

Raw skill file (markdown source)
# Responsive CSS Debugging

Use when a layout breaks on smaller screens — horizontal scrollbars, clipped content, overlapping elements — and you need to find and fix the actual root cause, not mask it.

## Step 1: Set a Real Device Viewport and Reproduce

Never debug at desktop width. Set the exact viewport of a real device.

### Chrome DevTools
1. Open DevTools (`F12` / `Cmd+Opt+I`)
2. Toggle device toolbar (`Cmd+Shift+M`)
3. Select a preset or enter dimensions: **iPhone 12: 390×844**, **iPhone SE: 375×667**, **iPad: 768×1024**

### Playwright (scriptable, reproducible)
```python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 375, "height": 667})
    page.goto("http://localhost:3000")
    page.screenshot(path="mobile.png", full_page=True)
    browser.close()
```

**Reproduce the break first.** Confirm the bug exists at the target viewport before touching any CSS.

## Step 2: The Overflow-Hunt Loop

Horizontal scroll on mobile is almost always one element wider than its container.

```javascript
// Run in the browser console — find the culprit
const doc = document.documentElement;
if (doc.scrollWidth > doc.clientWidth) {
  console.log("Overflow by", doc.scrollWidth - doc.clientWidth, "px");
  // Walk the DOM to find elements wider than the viewport
  document.querySelectorAll("*").forEach(el => {
    const rect = el.getBoundingClientRect();
    if (rect.right > doc.clientWidth + 1 || rect.width > doc.clientWidth) {
      console.log("OFFENDER:", el.tagName, el.className, rect.width, "px");
    }
  });
}
```

### Common Root Causes and Fixes

| Cause | Symptom | Fix |
|---|---|---|
| Fixed `min-width` | Element refuses to shrink | `min-width: 0` (flex children) or remove |
| `white-space: nowrap` | Text won't wrap | Allow wrap or `overflow-x: auto` on the right container |
| Fixed pixel width | `width: 800px` on mobile | `max-width: 100%` |
| Missing `box-sizing` | Padding pushes width past 100% | `box-sizing: border-box` globally |
| Flex row won't wrap | Items squeezed | `flex-wrap: wrap` |
| Long unbreakable token (URL) | One word overflows | `overflow-wrap: break-word` |

```css
/* Global reset that prevents most overflow */
*, *::before, *::after {
  box-sizing: border-box;
}

img, video, table, iframe {
  max-width: 100%;
}

pre, code {
  overflow-wrap: break-word;
  white-space: pre-wrap;
}
```

## Step 3: Media Query vs Fluid Layout

Decision table — choose the right tool:

| Situation | Use | Example |
|---|---|---|
| Layout fundamentally changes at a width (nav → hamburger) | Media query | `@media (max-width: 768px) { .nav { display: none } .menu-btn { display: block } }` |
| Element should just shrink proportionally | Fluid (%, rem, flex, grid) | `.card { width: 100% }` |
| Text size should scale | `clamp()` or rem | `font-size: clamp(1rem, 2.5vw, 1.5rem)` |
| Column count changes | Grid `repeat(auto-fit, minmax())` | `grid-template-columns: repeat(auto-fit, minmax(250px, 1fr))` |

```css
/* Fluid grid — no media query needed for the common case */
.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1rem;
}
```

## Measuring Computed Styles (not eyeballing)

```javascript
// Get the ACTUAL computed value, not what's in the stylesheet
getComputedStyle(el).width
getComputedStyle(el).fontSize
getComputedStyle(el).overflowX
```

Eyeballing "looks about right" is how layout bugs survive. Measure.

## Guardrails

- **Never** fix at desktop width and assume mobile follows — the bug lives at a specific viewport; reproduce it there.
- **Never** add a media query to mask an overflow — find and fix the overflowing element, not the symptom.
- **Never** use `!important` to force a fix — it hides the real specificity problem.
- **Always** test at the smallest supported viewport (375px is the common floor, but check your analytics).

## Pitfalls

- **Adding a media query that masks the real overflow**: Setting `overflow-x: hidden` on `body` hides the scrollbar but content is still clipped. Find the element, fix the width.
- **Testing only one viewport**: A fix for 375px can break 768px. Test the full range: 375, 768, 1024, 1440.
- **Fixed `min-width` on flex items**: The classic. Flex children default to `min-width: auto` and won't shrink below their content. Add `min-width: 0`.
- **Viewport meta tag missing**: Without `<meta name="viewport" content="width=device-width, initial-scale=1">`, mobile browsers render at desktop width and everything "breaks" in a way that has nothing to do with your CSS.
- **Debugging with screenshots at the wrong DPR**: A "mobile" screenshot at device-pixel-ratio 3 with no scaling looks different from a real device. Set the DPR too.

## Verify / Checklist

- [ ] Bug reproduced at the exact target viewport (375×667 or equivalent)
- [ ] `document.documentElement.scrollWidth <= clientWidth` at every supported viewport (375, 768, 1024, 1440)
- [ ] Root cause identified (a specific element + why), not just "added a media query"
- [ ] Global box-sizing reset present (`border-box`)
- [ ] Viewport meta tag present and correct
- [ ] Media queries and fluid techniques used for the right reasons (per decision table)
- [ ] Fix verified at multiple viewports, not just the one you fixed
- [ ] Computed styles measured, not eyeballed

Attached files

No attached files.