← All guides

/ mobile_app_design

MOBILE_APP

Mobile Breakpoints Are Device Categories, Not Arbitrary Pixels

Safe-area insets for notches, fluid vs. fixed mobile layout, and why guessing pixel widths breaks on half the phones on the market.

INTERMEDIATE · 13 MIN READ · MOBILE APP DESIGN
On this page
  1. The pixel habit worth breaking
  2. Why one pixel number keeps failing
  3. Categories, not devices
  4. Two breakpoint systems that already agree
  5. Picking your own breakpoints: content-out, not device-out
  6. Fluid vs. fixed inside a category
  7. Safe-area insets: designing for the notch
  8. The 100vh trap: dvh, svh, and lvh
  9. Container queries: the component-level complement
  10. Common mistakes
  11. The spec

01The pixel habit worth breaking

Open the media queries in almost any mobile stylesheet and you'll usually find a number with no real justification behind it: max-width: 375px because that's what an iPhone SE reported in a Figma frame two redesigns ago, or 768px because "that's what Bootstrap uses." The number gets typed once, works well enough on whatever device happened to be on the desk that day, and then calcifies — copied into the next component, the next project, eventually into a design system's official breakpoint token, with nobody left who remembers it was never actually derived from anything.

That's backwards from how the breakpoint systems teams actually rely on were built. Tailwind's five default breakpoints and Material Design's window size classes weren't reverse-engineered from one popular phone's screen size — they mark the widths at which a layout's underlying assumptions about the person holding it have to change, regardless of which specific device is doing the reporting. A breakpoint is a category boundary, not a device fingerprint, and the rest of this guide is about designing from that boundary instead of from a screenshot.

02Why one pixel number keeps failing

Part of the confusion is that "375px" doesn't mean what it sounds like it means. A CSS pixel is a density-independent unit — the browser has already scaled the screen's actual physical pixels down (or up) by the device's pixel ratio before your media query ever sees a number. A phone with a device pixel ratio of 3 might have well over a thousand physical pixels across its short edge and still report a CSS viewport width in the low 400s; a mid-range Android phone with a different pixel ratio can report a completely different CSS width while physically measuring almost the same size in your hand. The number in your media query was never a measurement of the glass — it's a measurement of the browser's layout viewport, and that's a softer, more negotiable thing than it looks.

It's softer for a second reason: it isn't fixed to one device, or even to one orientation of one device. Rotate a phone and the same physical screen reports a substantially wider CSS viewport — one that can land squarely inside the range a media query reserved for tablets. Unfold a book-style foldable and a phone-class device jumps into tablet-class viewport widths without ever leaving the user's hands. Drop a tablet into split-screen multitasking and it reports a viewport width closer to a phone's, even though a keyboard and a mouse might be sitting right next to it. Zoom the text larger for accessibility, and the effective layout width shrinks without the physical device changing at all. None of these are edge cases anymore — they're normal, frequent states for real people using real apps.

Chasing an individual device's width is chasing a target that is simultaneously moving (new phones ship every year) and multiplying (the same device can report several different widths depending on how it's currently being held). What's stable underneath all of that isn't a device — it's the category of interaction it currently represents.

03Categories, not devices

A device category is a cluster of viewport widths that share the same ergonomic and input assumptions, regardless of which product happens to be reporting that width in a given moment:

  • Compact — held in one hand, portrait more often than not, touch is the only realistic input, and reading happens close to the face. This is the thumb-zone world covered in the companion guide on reachability.
  • Medium — held with two hands or propped against something, touch is still primary but a keyboard or stylus is often nearby, and both portrait and landscape are equally normal.
  • Expanded — resting on a lap or a desk, a keyboard and a pointer with real precision are the primary input even when a touchscreen is also present, and the layout can afford genuine multi-column structure.

A breakpoint's job is to mark the width at which one of those assumptions stops holding — not the width at which "the next device in the lineup" happens to end. Framed that way, a breakpoint is a design decision about ergonomics that you can defend in a review, not a device chart you looked up and copied.

GREEN · COMPACT AMBER · MEDIUM RED · EXPANDED
Three ergonomic categories, each spanning a range of CSS pixel widths rather than one exact device. The boundaries sit near 600px and 840px because that's roughly where Tailwind's and Material 3's independently built breakpoint systems already agree a layout's assumptions have to change — see the comparison below.

04Two breakpoint systems that already agree

You don't have to invent these boundaries from nothing. Two of the most widely used breakpoint systems in the industry were built independently, for different platforms, by different teams — and they land in roughly the same place:

Tailwind's pixel scale vs. Material 3's window size classes
Category Tailwind breakpoint Material 3 window size class
Compact Below sm (< 640px) Compact (< 600dp)
Medium sm / md (640–1023px) Medium (600–839dp)
Expanded lg and up (≥ 1024px) Expanded (≥ 840dp)

They aren't identical — Tailwind's md at 768px sits inside Material's medium band rather than marking its own edge, and Material's dp unit isn't a 1:1 match for a CSS pixel. But the structural agreement matters more than the exact numbers: both systems put their first real category shift somewhere in the 600–768px range (roughly where a large phone in landscape or a small tablet in portrait sits) and their second in the 1024–1200px range (roughly where a tablet in landscape becomes indistinguishable from a small laptop). Two teams solving unrelated problems arriving at the same two edges is evidence those edges track something real about how screens get held, not just a shared convention.

Material also defines optional large (1200–1599dp) and extra-large (1600dp+) tiers, meant for genuinely multi-pane layouts on large tablets or external displays. Worth knowing they exist; rarely worth reaching for until you're actually building a three-pane dashboard or list-detail view that needs the extra room.

05Picking your own breakpoints: content-out, not device-out

The companion guide on responsive layout as a contract covers this principle in general; here it's worth spelling out for mobile specifically, because "mobile" is exactly where teams are most tempted to skip it and reach for a device chart instead.

Build the smallest layout first, using fluid units rather than fixed pixel widths. Then take a real, freely resizable browser window — not a fixed-width device frame in a design tool — and drag it wider a few pixels at a time. The moment the content itself starts to look cramped, lines get awkwardly short, or a component runs out of room before the surrounding whitespace does, that's where a breakpoint belongs — not at a round number, and not at whatever a device chart says. If that point happens to land near 600, 768, or 1024, round to the shared boundary, because landing near a widely-tested edge is a signal your content is expressing the same real shift everyone else's data already found. If it lands somewhere else entirely, trust the content over the round number — your layout, not a spec sheet, is the actual source of truth.

06Fluid vs. fixed inside a category

Landing on a category boundary isn't the finish line. Plenty of layouts get the breakpoint right and then lock every measurement inside that band to the one width they happened to test — which snaps awkwardly and leaves dead space at every other width within the same category. A 380px phone and a 420px phone are both "compact"; a layout hard-coded to 375px doesn't know that.

Keep the breakpoint itself reserved for structural changes — column count, navigation pattern, whether a sidebar exists at all — and let everything else flex continuously with fluid sizing:

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: clamp(12px, 3vw, 24px);
}

.section-padding {
  padding-inline: clamp(16px, 5vw, 48px);
}

auto-fit with minmax() lets the grid add or drop columns on its own as space changes, without a single extra media query. clamp() does the same for spacing and type — a fluid value between a floor and a ceiling — so the layout keeps adjusting smoothly between breakpoints instead of jumping between a small set of fixed states. The breakpoint still exists; it's just doing structural work instead of pixel-chasing work.

07Safe-area insets: designing for the notch

A phone's screen isn't a clean rectangle anymore. Camera cutouts, status-bar islands, rounded corners, and the home-indicator bar on iOS, or the gesture-navigation strip on Android, all carve out space that content can be clipped by or hidden behind if a layout assumes a perfect rectangle. This is a separate problem from breakpoints — it can bite a layout at any category, though it's most visible on compact phones.

The fix has two parts. First, opt in with viewport-fit=cover so the page is allowed to draw under those cutouts in the first place:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

Then pad anything pinned to an edge — a sticky header, a bottom tab bar, a floating action button — with the environment variables the platform exposes, keeping a sensible minimum for devices that don't need an inset at all:

.bottom-bar {
  padding-bottom: max(12px, env(safe-area-inset-bottom));
}
.top-bar {
  padding-top: max(8px, env(safe-area-inset-top));
}

Foldables add a related wrinkle without a fully standardized fix: the hinge or seam running across an unfolded screen. There's no env() variable with the same broad support as the safe-area insets above for querying that geometry yet, so the practical mitigation is simpler and more durable — avoid placing a single critical control, or a line of text, exactly across the visual midline of a layout that might be running on an unfolded device.

08The 100vh trap: dvh, svh, and lvh

100vh has a long-standing bug on mobile: a browser's address bar and toolbar expand and collapse as someone scrolls, but classic vh doesn't track that. The result is a full-height hero that's clipped at the bottom on first load, or one that leaves a jarring gap once the chrome retracts and the "full" height turns out to have been measured against a taller viewport than what was actually visible.

Three newer units replace the guesswork:

  • svh (small viewport height) — measured with browser chrome fully expanded. The safest default for anything that must never be clipped: a hero section, a full-screen modal, a bottom sheet.
  • dvh (dynamic viewport height) — tracks the chrome's current state live, which is the most visually "correct" number moment to moment but can introduce its own resize jank while scrolling if the layout depending on it isn't tested on a real device.
  • lvh (large viewport height) — measured with chrome fully collapsed. Mostly useful for a decorative background fill that should reach the true edge of the screen, rarely for anything a person needs to reliably reach or read.
.full-height-section {
  height: 100vh; /* fallback for older browsers */
  height: 100svh;
}

Support for all three is effectively universal by now (shipped in Chrome, Safari, and Firefox since 2023), but the plain vh fallback declared first — before the svh line overrides it — costs nothing and covers the remainder.

One more wrinkle: opening the on-screen keyboard shrinks the browser's visual viewport, but the layout viewport — and therefore every one of the units above — doesn't shrink to match unless the page opts in with interactive-widget=resizes-content in its viewport meta tag. Without that, a form field near the bottom of a 100svh section can still end up hidden behind the keyboard; track the visual viewport directly with the Visual Viewport API if a layout needs to react precisely to the keyboard opening.

09Container queries: the component-level complement

Everything above governs the page as a whole. Container queries solve a narrower, different problem: a single reusable component — a card, a profile row, a stat tile — that needs to look right based on the space it has been given, independent of how wide the overall viewport happens to be. The same card component can sit in a three-column grid on one screen and a narrow sidebar on another; a page-level breakpoint can't express that difference, because both placements can occur at the exact same viewport width.

.card-container {
  container-type: inline-size;
}

@container (min-width: 320px) {
  .card {
    grid-template-columns: 96px 1fr;
  }
}

Declare container-type: inline-size on the wrapping element, then write @container rules against its width the same way you'd write a media query against the viewport's. Support now exceeds 93% of global browser usage and degrades gracefully by design — a component without container-query support simply keeps its default, mobile-first styling, which is exactly the layout you'd want it to fall back to anyway.

Container queries don't replace the device-category breakpoints from earlier sections — the page still needs to know whether it's compact, medium, or expanded to decide on navigation pattern and overall structure. Container queries handle the layer underneath that: making a component reusable across regions of the same page without hard-coding it to any single context.

10Common mistakes

  • Naming a breakpoint after a specific device in code or comments — /* iPhone 14 breakpoint */ — when the phone it was named for will be discontinued long before the layout is retired, and that exact viewport width will keep showing up on unrelated phones, resized desktop windows, and folded devices for years after.
  • Testing exclusively in a fixed-size devtools emulator at 100% zoom, which reports classic vh and dvh identically because there's no real, collapsing browser chrome to disagree about — the exact bug this guide covers is invisible there and only shows up on an actual phone.
  • Treating "mobile" as a synonym for "narrow and portrait," and never checking a landscape phone, an unfolded foldable, or a tablet in split-screen — all of which can report a medium- or expanded-width viewport while still being touch-first, one-handed, and thumb-driven in every way that actually matters to the design.
  • Locking a hero section to 100vh once and never revisiting it — the layout looks fine in the first screenshot and then quietly clips or gaps the moment someone scrolls, rotates, or opens the keyboard.

11The spec

// mobile_breakpoints.spec

Compact
0–599px — one-handed, portrait-first, touch-only assumptions
Medium
600–839px — two-handed or propped, touch-primary
Expanded
840px+ — pointer and keyboard primary
Method
Content-out — grow a real browser window until the content breaks, round to a shared boundary only if it lands nearby
Full-height sections
100svh with a plain 100vh fallback — never 100vh alone
Safe area
viewport-fit=cover plus env(safe-area-inset-*) padding on every edge-pinned element
Component-level fit
Container queries, not additional page breakpoints
Validate on
A real device, both orientations, with the on-screen keyboard opened at least once

None of this replaces resizing a real browser window on a real device — the category boundaries above are a starting point tuned to today's screens, not a permanent law. Set them once from content and ergonomics rather than from a single device's width, and a layout stops breaking every time next year's phone ships a few pixels wider or narrower than the one it was tested on.