mobile-feel
A phone is not a small desktop. The pointer is a thumb, the viewport moves, the keyboard eats half the screen, and every mistake is felt in the hand.
Targets
- 44×44px minimum for anything tappable, even if it draws at 20px. Extend the hit area with padding or a pseudo-element, not by making the icon bigger.
- 8px minimum between adjacent targets.
- Primary actions in the bottom third of the screen, where a thumb rests. Destructive actions away from it.
- Rows are the target, not the text inside them.
Viewport
100dvh, never100vh.vhincludes the browser chrome;dvhis what is actually visible. Usesvhfor things that must not resize when the chrome hides.<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">.viewport-fit=coverunlocks safe-area insets. Neveruser-scalable=noormaximum-scale=1.- Safe areas on every fixed or sticky edge element:
.bottom-bar { padding-bottom: max(12px, env(safe-area-inset-bottom)); }
.top-bar { padding-top: env(safe-area-inset-top); }
Touch behaviour
* { -webkit-tap-highlight-color: transparent; }
button, a, [role="button"] { touch-action: manipulation; } /* no 300ms delay, no double-tap zoom */
.drag-handle { touch-action: none; } /* only on the handle */
.scroll-region { overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
- Replace the removed tap highlight with your own
:activestate, or taps feel dead. :hoverstyles stick after a tap on touch devices. Gate every hover with@media (hover: hover).:activeis unreliable on iOS for elements without atouchstartlistener; a data attribute set onpointerdownand cleared onpointerup/pointercancelis more dependable.- Long-press selects text and shows a callout on links;
-webkit-touch-callout: noneanduser-select: noneon controls only, never on content.
Inputs
- Font size 16px or larger on inputs, or iOS zooms the page on focus and does not zoom back.
inputmode="numeric","email","tel","decimal","search"so the right keyboard appears.autocompletevalues set (email,one-time-code,cc-number…).autocapitalize="none"on usernames and codes.enterkeyhint="send"/"search"/"done"on the last field.- Focused inputs must be visible above the keyboard. Listen to
visualViewportresizeand scroll the field into view, or anchor the composer tovisualViewport.height. - Fixed elements move when the keyboard opens on iOS. Anything that must stay put uses
position: absoluteinside a container sized to the visual viewport.
Sheets
Bottom sheets are the phone's dialog:
- Drag to dismiss, following the finger 1:1. Release past ~35% or with downward velocity dismisses; otherwise snap back.
- Snap points where the content has natural stops (peek, half, full).
- The backdrop's opacity tracks the drag.
- Scroll inside the sheet only when it is at its full snap; before that, a drag scrolls the sheet, not the content.
- Body scroll locked without layout shift.
- A grabber at the top, 36×5px, at
--fg-4. - Use a library that has solved the physics (Vaul) rather than writing it.
Full-screen dialogs on a phone slide up from the bottom; on desktop they fade in centred. It is the same component with two presentations.
Bottom navigation
- 3–5 items, icon and label, 49–56px tall plus safe area.
- Active item in
--fg, others in--fg-3. No accent fill. - Tapping the active item scrolls its view to top.
- It hides on scroll-down and reappears on scroll-up only if the content needs the space; otherwise it stays.
Scroll
- One scrolling region per screen where possible. Nested scroll on a phone is a trap.
- Pull-to-refresh: either implement it properly or set
overscroll-behavior-y: containon the scroller so it does not fire the browser's. - Horizontal carousels:
scroll-snap-type: x mandatory,scroll-paddingfor the gutters, and a visible cue that there is more. - Sticky headers under 56px. Anything taller steals the screen.
- No smooth-scroll libraries on touch. Native momentum is the reference; do not compete with it.
Feedback
- Press states are larger on touch than on desktop: scale 0.96 instead of 0.98, or a visible fill, because there is no hover to prepare the eye.
- Haptics are unavailable on the web (
navigator.vibrateis Android-only and coarse). Compensate with visual feedback on the same frame. - Toasts at the bottom-centre, above the bottom bar and the keyboard.
- Loading indicators near the thumb, not at the top of a screen the thumb is covering.
Layout at 375px
- Side gutters 16px. Never 0, never 24+.
- One column. Cards full width with 12px radius, or edge-to-edge rows with dividers; not both.
- Text at 15–16px body, 13px secondary. 13px body is too small for a phone held at arm's length.
- Tables become cards or a horizontal scroll with a pinned first column. Never a squashed table.
- Truncate less; wrap more. Names wrap to two lines on a phone.
- Test with the largest dynamic type setting and with 200% zoom.
Performance on a real phone
- Test on a mid-range Android, not the newest iPhone. Throttle 4× CPU in DevTools as a proxy.
backdrop-filterand large box-shadows are the first things to cut when scroll stutters.- Animate only
transformandopacity. Neverheight,widthortopon a phone. - Images sized for the device (
srcset), lazy below the fold, decoded async. - First interaction ready under 2s on 4G: defer everything that is not the first screen.
PWA touches
manifest.webmanifestwith name, icons (192, 512, maskable),display: standalone,theme_color,background_color.apple-mobile-web-app-capableand the status bar style.- A splash that matches the app's frame colour, not white.
Checklist
- 44px targets, 8px apart, primary actions low on the screen
-
dvh,viewport-fit=cover, safe areas on fixed edges - Tap highlight removed and replaced with an
:activestate - Hover gated; no sticky hover after tap
- Inputs at 16px with
inputmode,autocomplete,enterkeyhint - Focused field visible above the keyboard
- Sheets drag, snap, and lock body scroll
- One scroll region; overscroll contained
- Toasts above the bottom bar
- Tested on a mid-range Android with throttling
From the catalogue
- loadmo.re — mobile-first sites and experiments worth studying.
- Vaul — the drawer with the gesture solved.
- Apple Human Interface Guidelines — targets, sheets, safe areas, as Apple specifies them.
- Mobbin — real mobile flows, searchable by pattern.
- web.dev — INP, viewport units and touch guidance.
- More at stealth.pm/look.