Skip to content

Mobile Patterns

One hand, one thumb, an unreliable network and a screen the system partly owns. Every mobile rule follows from those four facts.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground
Alerts
44px rows

The floor. A fingertip contact patch is roughly 8–10mm, and 44px is what covers it with room for the imprecision of a moving hand.

Safe area respected: the bar sits above the home indicator.

The thumb zone

The arc a right thumb sweeps from the bottom corner. Everything a user does often belongs inside it; anything destructive belongs outside it.

Hard to reach — needs a grip change
A stretch
Comfortable — put the actions here
Consequences

Navigation goes to the bottom, not the top.

The primary action is full-width at the bottom of a form.

Back lives at the top-left because the system put it there — and an edge swipe does the same job in the comfortable zone.

Destructive actions go where the thumb does not rest by accident.

Swipe actions

Drag the row left. It is fast for people who know it, invisible to everyone else — so the same two actions must also exist somewhere visible.

Billing webhook failed

Note what it does not do: it never swipes from the screen edge, because that gesture belongs to the system’s back navigation.

Pull to refresh

Drag the list down. The indicator tracks the finger, so the gesture explains itself before it commits — and it still needs a visible refresh control elsewhere.

Activity
  • Deployment finished
  • Billing webhook failed
  • New comment from Grace
  • Sprint 14 closed
  • Invoice #4021 paid
  • Backup completed

Safe areas

The home indicator owns the bottom band on a gesture-navigation phone. A bar that ignores it puts its targets under the swipe that closes the app.

Respected
  • Deployment finished
  • Billing webhook failed
  • New comment from Grace
  • Sprint 14 closed
  • Invoice #4021 paid
  • Backup completed
padding-block-end: env(…)
Ignored
  • Deployment finished
  • Billing webhook failed
  • New comment from Grace
  • Sprint 14 closed
  • Invoice #4021 paid
  • Backup completed
bar under the home indicator

Adapting desktop layouts

Master–detail becomes list-then-page with a back control. Two squeezed columns serve neither.

Alerts
  • Deployment finished
  • Billing webhook failed
  • New comment from Grace
  • Sprint 14 closed
  • Invoice #4021 paid
  • Backup completed
List
Billing webhook
Failed

Three delivery attempts failed with a 500 from the endpoint. The next retry is in eleven minutes.

Then page

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

44px
44px target
32px
32px target
24px
24px target
Retry
Pressed
Swiped
Refreshing
No connection
Offline
Safe area

Anatomy

Every part, every measurement, and the reason it is that number.

Alerts
  • Deployment finished
  • Billing webhook failed
  • New comment from Grace
  • Sprint 14 closed
  • Invoice #4021 paid
  • Backup completed

A phone screen and the zones it is really divided into. The layout on the left only works because it agrees with the map on the right.

  1. Status bar / notchenv(safe-area-inset-top)

    The system’s. Content underneath it is clipped on some devices and hidden behind a camera on others. Never hardcode its height — it differs per handset.

  2. Top bar44–56px

    Title and at most two actions. It is in the hard-to-reach zone, which is why frequent actions do not live here — only Back does, because the system put it there.

  3. Content wellthe scrolling region

    One scroll context. Nested scrolling areas on touch are near-impossible to control with a thumb and should be designed out.

  4. Row height44px minimum

    The floor for anything tappable. Padding counts toward it — the visible mark can be small provided the hit area is not.

  5. Thumb arcbottom third

    The comfortable zone for a one-handed grip. Everything used often belongs inside it; anything destructive belongs outside it.

  6. Bottom navigation56px + inset

    Three to five destinations, inside the arc, with labels. The inset is added below the bar, not absorbed into its height.

  7. Home indicatorenv(safe-area-inset-bottom)

    Roughly 34px on a gesture-navigation phone, and it is not yours. A target here competes with the swipe that closes the app.

  8. Edge gutters16px

    Content clears the edge because a curved screen distorts the last few pixels — and because the hand wrapping the phone rests there.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Spacing

TokenValueUsed for
touch targetThe floor for anything tappable
safe-area-inset-topStatus bar and notch
safe-area-inset-bottomHome indicator
edge gutterContent inset from the screen edge
row heightList rows
bottom barNavigation

Typography

TokenValueUsed for
--text-bodyBody text. 16px on inputs, to stop iOS zooming

Motion

TokenValueUsed for
durationPage transitions
gesture thresholdSwipe and pull commit points

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightPaddingTypeMin widthTouch targetWhen to use
Touch target44px——44px44pxThe floor. Padding counts.
Comfortable target48px———48pxPrimary actions and anything used one-handed while walking.
List row44–56px0 16px———Single or two-line rows.
Top bar44–56px————Plus the top safe-area inset.
Bottom nav56px————Plus the bottom safe-area inset.
Edge gutter—16px———Both sides. Never flush to a curved edge.
Input font size——16px——Below 16px, iOS zooms the page on focus and does not zoom back.
Swipe threshold———64px—Past this, the action commits on release.
Pull threshold56px————Past this, the refresh fires on release.

Do

Put the primary action in the thumb arcThe bottom third is where a one-handed grip can reach without moving the phone. Everything used often belongs there; the top-right corner is the worst place on the screen.
Make the hit area bigger than the markA 16px icon can have a 44px target. Padding counts, so there is no conflict between a clean visual and a forgiving one.
padding-block-end: max(1rem, env(safe-area-inset-bottom))
Use env() for every system insetThe notch and the home indicator differ per device and change between generations. A hardcoded 34px is right on one handset and wrong on the next.
swipe · and a real button
Give every gesture a visible twinSwipe-to-archive is invisible until someone tries it. The same action must exist in a menu or a detail view, or a large share of users will never have it.
input { font-size: 16px }
Set inputs to 16pxiOS zooms the whole page when a field smaller than 16px takes focus, and does not zoom back out. It is the single most common mobile form bug.
Queued · will send when online
Design the offline statePhones lose signal in lifts and tunnels mid-action. Queue the write, show what is pending, and never let a failure look like data quietly disappearing.

Don't

Do not put frequent actions in the top cornersReaching the top-left of a large phone one-handed means shifting your grip, and dropping the phone is a real cost. Back lives there only because the system put it there.
edge-swipe carousel → user leaves the app
Do not take the system’s gesturesAn edge swipe is back on both platforms and a bottom swipe is home. A carousel that starts at the screen edge is a carousel that navigates away instead.
title="…" → never seen on touch
Do not rely on hoverThere is none. A tooltip, a hover preview or a reveal-on-hover row action simply does not exist on a phone, and the first tap will fire whatever is underneath.
Do not shrink a desktop tableSix columns at 320px is unreadable and horizontally scrolling tables are painful with a thumb. Turn each row into a card carrying only the fields that matter.
long-press-only delete → undiscoverable
Do not make a gesture the only wayWCAG 2.5.1: any path-based or multi-point gesture needs a single-pointer alternative. It is also plain sense — most users never discover a gesture nobody showed them.
Do not place a destructive action under the thumbThe comfortable zone is where accidental taps land. Delete belongs where the thumb has to travel deliberately, and it belongs behind an Undo.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.3.4OrientationAA1.4.10ReflowAA2.5.1Pointer GesturesA2.5.2Pointer CancellationA2.5.8Target Size (Minimum)AA1.4.4Resize TextAA

Contrast

  • Mobile screens get used outdoors. Contrast that passes indoors at 4.5:1 can be unreadable in sunlight, so treat the minimum as a minimum rather than a target.
  • Do not lower contrast to fit more on screen. If it does not fit at readable contrast, it does not fit.
  • Test with the display at low brightness and with a smudged screen. Both are the normal condition, not an edge case.

Keyboard

External keyboardPhones and tablets support them. Every control still needs to be reachable and focusable.
Switch controlSteps through focusable elements one at a time — which is why gesture-only actions exclude people entirely.
Escape / BackThe system back gesture must dismiss the topmost layer, exactly as Escape does on desktop.

Screen readers

  • VoiceOver and TalkBack take over the touch gestures entirely. Any custom gesture must have a real control behind it, or it does not exist under a screen reader.
  • Announce refresh states. A spinner conveys nothing; "Refreshing" and then "Updated, 6 items" conveys the same thing the animation does for everyone else.
  • Group list rows sensibly. A row read as six separate fragments is exhausting to move through at speed.
  • Respect the system text size. A user at 200% text has told the operating system something important, and a layout that clips at that size is a reflow failure.

Focus & touch

  • The software keyboard covers up to half the screen. Scroll the focused field into the remaining space, and never let the keyboard hide the submit button — that is the single most common mobile form failure. Use dvh rather than vh so the layout tracks the visible viewport instead of an idealised one.
  • This entire page is the touch section. The short version: 44px targets, act on pointer-up, respect the safe areas, never require a gesture, keep frequent actions in the bottom third, and keep destructive ones out of it.
AttributeApplied toNotes
Target sizeEvery controlWCAG 2.5.8 sets 24px as the AA floor; 44px is the practical one. The spacing exception only helps if the neighbours are genuinely far enough away.
Pointer cancellationEvery buttonWCAG 2.5.2: fire on pointer-up, not pointer-down, so a user can slide off a mis-aimed control to abort. Never act on touch-start.
Gesture alternativeSwipe and long-press actionsWCAG 2.5.1 requires a single-pointer path to anything a gesture does. A visible button satisfies it.
OrientationThe appWCAG 1.3.4: do not lock to portrait. Someone with a mounted device may have no choice about how it is held.
aria-liveRefresh and offline statesA spinner that means nothing to a screen reader needs "Refreshing" and "Updated" announced politely.

Code

Example usage

tsx
1// Adapt the container, not just the width. Master–detail becomes2// list-then-page — two squeezed columns serve neither side.3const isPhone = useMediaQuery('(max-width: 639px)')45return isPhone6  ? selected7    ? <DetailPage item={selected} onBack={() => setSelected(null)} />8    : <List onSelect={setSelected} />9  : <MasterDetail selected={selected} onSelect={setSelected} />1011// Every gesture needs a visible twin. The swipe is the accelerator;12// the button is the interface.13<SwipeableRow14  onSwipeLeft={archive}15  actions={<IconButton label="Archive" icon={<Archive />} onClick={archive} />}16/>1718// Act on pointer-up so a mis-aimed tap can be slid off and aborted.19// WCAG 2.5.2, and also just how people expect buttons to behave.20<button onPointerUp={submit}>Retry now</button>   // not onPointerDown2122// The keyboard covers up to half the screen. dvh tracks the visible23// viewport; vh is an idealised one the user may never actually have.24<div className="h-dvh">…</div>

CSS

css
/* The floor. Padding counts toward it, so a 16px icon is fine. */
.ds-touch-target {
  min-block-size: 44px;
  min-inline-size: 44px;
  display: grid;
  place-items: center;
}

/* The system's bands are not yours, and their size differs per
   device. max() keeps a sensible minimum on handsets with no inset. */
.ds-screen {
  padding-block-start: env(safe-area-inset-top);
  padding-inline: max(16px, env(safe-area-inset-left));
}
.ds-bottom-bar {
  padding-block-end: max(12px, env(safe-area-inset-bottom));
}

/* Below 16px, iOS zooms on focus and never zooms back. */
input, select, textarea { font-size: 16px; }

/* Let the browser own the axis it needs; claim only the other one.
   touch-action: none on a scrollable region breaks scrolling. */
.ds-swipe-row  { touch-action: pan-y; }
.ds-pull-list  { touch-action: pan-x; }

/* dvh tracks the visible viewport as the address bar collapses and
   the keyboard opens. vh does not. */
.ds-app { block-size: 100dvh; }

/* Tap highlight is the platform's; ours is a real pressed state. */
.ds-button {
  -webkit-tap-highlight-color: transparent;
  transition: transform 90ms ease-out;
}
.ds-button:active { transform: scale(0.97); }

/* No hover on touch — so anything hidden behind it must come back. */
@media (hover: none) {
  .ds-row__actions { display: flex; }
}

Component API

Gesture contract

PropTypeDefaultDescription
thresholdnumber64–110pxTravel before the action commits. Far enough that a scroll never triggers it by accident.
touchAction*'pan-x' | 'pan-y'—Claim one axis and leave the browser the other. `none` on a scrollable region breaks scrolling.
alternative*ReactNode—The visible control that does the same thing. Not optional — WCAG 2.5.1.
onCancel() => void—Sliding back below the threshold must abort cleanly. Pointer cancellation, WCAG 2.5.2.

Safe area

PropTypeDefaultDescription
viewport-fit=cover*meta tag—Required before env(safe-area-inset-*) reports anything but zero.
env(safe-area-inset-top)CSS—Status bar and notch. Differs per device — never hardcode it.
env(safe-area-inset-bottom)CSS—Home indicator, roughly 34px on gesture-navigation phones.

Notes

Professional tips

  • Test one-handed on the largest phone you support. The top-left corner of a 6.7-inch screen is genuinely out of reach, and no amount of desk testing reveals that.
  • Test with a thumb, not an index finger. Pointing at a screen you are holding flat is a completely different level of precision from how the device is actually used.
  • Add viewport-fit=cover to the meta tag or env(safe-area-inset-*) reports zero everywhere and the layout looks fine right up until it ships.
  • Prefer a bottom sheet to a dialog on every mobile surface. It arrives where the thumb is and leaves the same way.
  • Undo beats confirm. A snackbar with Undo costs one tap when wrong; a confirmation dialog costs one tap every single time.
  • Keep tap animations under about 100ms. Anything slower reads as lag rather than as feedback, and on touch the feedback is all the user has.

Performance

  • Mobile CPUs are several times slower than the laptop you are testing on, and the network is worse. Budget accordingly rather than optimistically.
  • Animate transform and opacity only. Anything else drops frames on a mid-range Android, which is the median device for most products.
  • Virtualise long lists and keep images sized. Layout shift on a phone moves the thing someone was about to tap.
  • Use passive scroll listeners. A non-passive touchmove handler blocks scrolling on the main thread and produces the classic sticky-scroll feel.
  • Serve smaller images to smaller screens. A 2000px hero on a 390px phone is bandwidth spent on a device that has the least of it.
  • Cache aggressively and render from cache first. On a slow connection, showing yesterday’s list instantly beats showing a spinner for four seconds.

Common mistakes

  • Targets under 44px, inherited straight from the desktop layout.
  • Ignoring safe areas, so the last row sits under the home indicator.
  • Gestures with no visible equivalent, invisible to most users and impossible under a screen reader.
  • Hover-dependent interfaces, where the first tap fires the thing underneath.
  • Inputs under 16px, causing iOS to zoom and never zoom back.
  • vh instead of dvh, so the keyboard covers the submit button.
  • Horizontally scrolling tables that a thumb cannot control.
  • Destructive actions in the comfortable zone, where accidental taps land.
  • Acting on pointer-down, so a mis-aimed tap cannot be aborted.

Real-world recommendations

  • Check the analytics before arguing about this. In most products mobile is the majority of sessions, and it is usually still designed second.
  • The median device is a mid-range Android two or three years old, not the phone in your pocket. Test on one; the difference is not subtle.
  • Every gesture you add is a feature for the people who find it and nothing at all for everyone else. Add them freely — just never as the only route.
  • One-handed use while walking, in sunlight, on a bad connection is the real environment. Any of those alone breaks a design tested at a desk.
  • The safe-area inset is the most commonly skipped rule on this page, and it is the one users notice immediately — a button they cannot press without closing the app.