Web Development
Mobile Responsiveness: Fix Broken Layouts from 320px to 3440px
Fix mobile responsiveness bugs: fluid grids, container queries, dvh units, iOS input zoom, tap targets, safe areas, and Playwright overflow tests.
Tuesday morning, two screenshots landed in the same Slack thread.
The first came from a PM with a new 34-inch ultrawide. Our pricing page stretched edge to edge. Headline lines ran past 300 characters, and one lonely card floated in a desert of white space.
The second was a support ticket from a customer on an iPhone SE: "I tap the card number field and the whole page zooms in. I can't find the Continue button."
Same page. Same CSS. Two broken experiences, about 3,000 pixels apart. QA had signed off, because QA tested a 390px simulator and a 1440px laptop. Nobody owned the weird edges.
Quick answer: Mobile responsiveness is a page's ability to adapt its layout, type, and touch targets to any viewport width and input method. Build it with fluid CSS (Grid with
minmax(),clamp(), and container queries) instead of device-specific breakpoints. Keep form inputs at 16px, size tap targets to 44 CSS pixels, usedvhinstead ofvh, and test for horizontal overflow at several widths in CI.
What causes mobile responsiveness bugs?#
Most responsive bugs come from layouts that assume a viewport size instead of reacting to the space they actually get. Add touch input and a few browser-specific behaviors, and the edges break first. Four causes cover the majority of tickets.
Breakpoints tied to a device list. Someone writes @media (max-width: 390px) because that's the iPhone in the Figma file. Then a foldable ships. Then a tablet in split-screen. Devices multiply faster than your media queries, and a layout that stops at 1440px has no opinion about what happens at 3440px. It just stretches.
Horizontal overflow. One element wider than the viewport adds a sideways scrollbar to the whole page. The usual suspects: width: 100vw (which includes the scrollbar's width on desktop, so you get a roughly 15px overflow for free), fixed-width tables, images without max-width, and long unbroken strings like URLs. Flex and grid items also default to min-width: auto, so they refuse to shrink below their content's width. One long token inside a card can blow out the entire grid.
Viewport and keyboard behavior. On mobile, 100vh is measured against the largest viewport, the one with browser toolbars retracted. With toolbars showing, content spills under them. iOS Safari also zooms into any focused input whose font size is under 16px, which is exactly what our customer hit. And a position: fixed footer can end up hidden behind the on-screen keyboard.
Touch is not a mouse. Fingers are blunt. A 28px icon button works fine with a cursor and fails constantly with a thumb. Menus that open on :hover never open at all on a phone. And on a notched device, content pinned to the bottom edge can sit under the home indicator.
Which layout approach should you use?#
Default to intrinsic layout for the page and container queries for components. Reach for viewport media queries only for page-level shell changes, like swapping a sidebar for a drawer.
| Approach | Best for | Breaks when | Verdict |
|---|---|---|---|
| Viewport media queries | Nav, sidebar, page-level columns | One component appears in slots of different widths | Keep, use sparingly |
| Container queries | Reusable cards, widgets, form groups | The parent's size depends on its children | Default for components |
Intrinsic layout (auto-fit, clamp(), min()) | Grids, type, spacing | Design demands exact column counts at fixed sizes | Default for layout |
Device targeting (UA sniffing, m. subdomain) | Nothing worth the upkeep | A new device ships next month | Avoid |
How do you build a layout that survives 320px to 3440px?#
1. Set the viewport meta tag, and never block zoom#
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
viewport-fit=cover lets you paint under the notch, which you'll handle with safe-area insets in step 6. Don't add user-scalable=no or maximum-scale=1 to "fix" the iOS zoom. It blocks pinch zoom for people who need it, and accessibility audits flag it.
2. Let the content size the layout#
.page {
width: min(100% - 2rem, 80rem);
margin-inline: auto; /* ultrawide stops stretching here */
}
.grid {
display: grid;
gap: clamp(1rem, 2vw, 2rem);
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
}
.card { min-width: 0; } /* lets long strings shrink instead of blowing out the grid */
h1 { font-size: clamp(1.75rem, 1.2rem + 2.5vw, 3.5rem); }
.prose { max-width: 70ch; } /* readable line length at any width */
That min(100%, 18rem) inside minmax() matters. Without it, a column with an 18rem minimum overflows a 320px screen. Notice there isn't a single media query here, and the grid still goes from one column to many on its own.
3. Use container queries for components#
A card shouldn't care how wide the window is. It should care how wide its slot is. Container queries are supported in all current major browsers; see the MDN container queries guide.
.card-wrap { container-type: inline-size; }
.card { display: grid; gap: 1rem; } /* stacked by default */
@container (min-width: 32rem) {
.card { grid-template-columns: 12rem 1fr; }
}
The same card now works in a narrow sidebar and a wide main column with zero extra code.
4. Fix forms for thumbs#
input, select, textarea {
font-size: max(16px, 1rem); /* below 16px, iOS Safari zooms on focus */
min-height: 2.75rem;
}
<input type="text" name="card" inputmode="numeric"
autocomplete="cc-number" pattern="[0-9 ]*">
inputmode summons the numeric keypad. autocomplete lets the browser fill the field. Both cut typing, which is the slowest thing a person does on a phone.
5. Size tap targets and gate hover#
WCAG 2.2 requires targets of at least 24 by 24 CSS pixels at level AA. For real thumb comfort, aim for 44 by 44, which matches Apple's guidance (Material Design says 48dp), with some space between neighbors.
.btn, .icon-btn {
min-block-size: 44px;
min-inline-size: 44px;
}
@media (hover: hover) and (pointer: fine) {
.nav-item:hover .submenu { display: block; }
}
Keep a tap and keyboard path to anything a hover reveals. The media query only adds the hover behavior where a real pointer exists.
6. Use dynamic viewport units and safe areas#
There are now three viewport heights: svh (small, toolbars showing), lvh (large, toolbars hidden), and dvh (dynamic, tracks the current state). The web.dev write-up on the new viewport units covers the details.
.app-shell {
min-height: 100vh; /* fallback for older engines */
min-height: 100dvh;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.form-actions {
position: sticky; /* tends to behave better than fixed around the keyboard */
bottom: 0;
padding-bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
background: var(--bg);
}
7. Hunt overflow in the console, then lock it in CI#
Paste this into DevTools to list whatever is poking past the right edge:
const w = document.documentElement.clientWidth;
[...document.querySelectorAll('body *')]
.filter((el) => el.getBoundingClientRect().right > w)
.forEach((el) => console.log(el));
Then make the build fail when someone reintroduces the problem:
import { test, expect } from '@playwright/test';
const widths = [320, 390, 768, 1280, 3440];
for (const width of widths) {
test(`no horizontal overflow at ${width}px`, async ({ page }) => {
await page.setViewportSize({ width, height: 900 });
await page.goto('/pricing');
const overflow = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth
);
expect(overflow).toBeLessThanOrEqual(0);
});
}
Run this on your top landing pages and checkout flow first. If you've already set up cross-browser projects, add these widths to that matrix.
Does mobile responsiveness affect SEO?#
Yes, indirectly but concretely. Google uses mobile-first indexing, which means it primarily crawls and indexes the mobile version of your pages. Content, structured data, and links that exist on desktop but not on mobile may not get indexed at all.
What to do about it:
- Serve one URL with one set of content. Responsive CSS on a single URL is the simplest setup. Separate
m.subdomains double your parity problems. - Keep mobile and desktop content equal. Collapsing text into an accordion is fine. Removing it is not.
- Don't hide content behind interactions a crawler can't perform. If it only loads after a tap or a swipe, assume it won't be seen.
- Watch your vitals. A layout that shifts or lags on phones also drags down the page speed numbers you're measured on.
Google retired its standalone Mobile-Friendly Test and the Search Console mobile usability report in late 2023, so Lighthouse and your own field data are what you've got now.
What does this cost you?#
Nothing above is free.
clamp()can hurt accessibility if misused. Type sized purely invwignores user zoom. Always mix in aremterm, as in theh1example, so text still scales with browser settings.- Container queries add containment.
container-type: inline-sizemakes the element's inline size independent of its children. Layouts that relied on a parent shrink-wrapping its content can collapse. Test it on real components before a global rollout. - Intrinsic grids are less predictable.
auto-fitdecides the column count for you. Designers who want exactly three columns at exactly 1024px will push back, and sometimes they're right. A targeted media query is fine there. - Big tap targets cost density. Data-heavy tables and dashboards suffer at 44px rows. Use the 24px AA minimum with adequate spacing, or offer a density toggle.
dvhtriggers re-layout. As toolbars collapse during scroll, anything sized indvhresizes with them, which can look jittery.svhis stable but leaves a gap when toolbars hide. Pick per component.- Emulators lie. DevTools can't reproduce the iOS keyboard, toolbar collapse, or real touch precision. Budget for a few physical phones.
Frequently asked questions#
What breakpoints should I use for responsive design?#
Let the content decide. Start with a single-column fluid layout, widen the browser until the design looks stretched or cramped, and add a breakpoint there. Fewer is better: auto-fit grids, clamp(), and container queries handle most adaptation without any.
What is the minimum tap target size?#
WCAG 2.2 requires at least 24 by 24 CSS pixels at level AA (Success Criterion 2.5.8). For comfortable thumb use, aim for 44 by 44 pixels per Apple's guidance, or 48 density-independent pixels per Material Design, with space between neighbors.
Why does iOS Safari zoom in when I tap an input?#
Safari on iOS zooms into any focused input whose font size is below 16px. Set inputs to at least 16px and leave pinch zoom enabled instead of blocking it with maximum-scale=1.
Should I use vh or dvh?#
Use dvh when an element should fill the visible screen as mobile toolbars appear and disappear. Use svh when you want a stable size that never gets cut off. Keep a vh declaration above either one as a fallback for older browsers.
How do I fix horizontal scrolling on mobile?#
Find the element that extends past the viewport (the console snippet earlier in this article lists them). Common causes are width: 100vw on a page with a scrollbar, fixed-width tables or images, long unbroken strings, and flex or grid children missing min-width: 0.
How do I test responsive design properly?#
Combine three layers: automated Playwright checks for overflow at several widths, browser DevTools emulation for fast iteration, and a few real phones for the touch, keyboard, and toolbar behavior that emulators don't reproduce.
Before the next release#
Grab the narrowest phone and the widest monitor you can find. Open your top landing page and your checkout, tap into every form field, and scroll to the bottom with the keyboard up. It takes ten minutes. Then add the five-width overflow test to CI tonight, so the next person who ships a 1px problem finds out from a red build and not from a customer's screenshot.