Reference · UX pattern catalogueMobile patterns

32 patterns in this category — each with its illustration, what it is, and the fields that matter when you build or review one. Part of the UX pattern catalogue; the raw, agent-readable version is ux-pattern-catalogue/mobile-patterns.json.

Touch

STRETCH REACH THUMB ZONE Action

Thumb Zone Design mobile-001

Place primary actions in the bottom third of the screen where thumbs naturally rest. The top of the screen is the stretch zone — reserve it for less-frequent actions or information display. The "thumb zone" principle applies to all one-handed mobile usage.

Implementation

Position CTAs, navigation, and primary inputs at the bottom. Use `env(safe-area-inset-bottom)` to pad above the home indicator. Test by holding the phone with one hand — if you can't reach a primary button, move it down.

Accessibility

Bottom placement benefits motor-impaired users who may have limited reach. Ensure actions remain visible to screen magnification users by not placing them at the extreme edge.

x 16px x 48px ::after hit area finger

Touch Target Sizing mobile-002

Minimum 48x48dp (Material Design) or 44x44px (WCAG 2.5.8). Small targets cause mis-taps, especially for users with motor impairments. The most common mobile usability failure.

Implementation

Set `min-width: 48px; min-height: 48px` on interactive elements. For small icons, expand the hit area using a `::after` pseudo-element with `position: absolute; inset: -8px`. Never rely on visual size alone.

Accessibility

WCAG 2.5.8 (Target Size Minimum) requires 24x24px minimum with no adjacent targets within 24px. WCAG 2.5.5 (Target Size Enhanced, AAA) requires 44x44px. Use the larger target.

Entire card tappable display: block on anchor Only link text tappable Dead space frustrates users

Large Tap Areas mobile-003

Make entire rows, cards, and list items tappable — not just the text or icon inside them. Users expect the full surface to be interactive. Tapping dead space next to a link and getting no response frustrates users.

Implementation

Use `display: block` on anchor elements wrapping cards. For list items, apply `cursor: pointer` and click handler to the `<li>`, not a nested element. Ensure the entire visual container responds to touch.

Accessibility

The tappable element must be a semantic link (`<a>`) or button. If the card has multiple actions, use one primary link wrapping the card and `position: relative` + `z-index` for secondary actions.

Too close Btn A Btn B 0px! 8px gap Btn A Btn B gap: 8px Prevents ghost touch errors

Minimum Spacing Between Targets mobile-004

Maintain at least 8px of non-interactive space between adjacent touch targets. Without spacing, users accidentally trigger the wrong action — the "ghost touch zone" problem, especially common in dense lists and toolbars.

Implementation

Use `gap: 8px` in flex/grid containers with interactive elements. For inline buttons, add `margin-inline: 4px`. Test with large-finger simulation (Chrome DevTools touch emulation at 2x DPR).

Accessibility

WCAG 2.5.8 allows undersized targets if they have 24px spacing. Generous spacing is the simplest way to meet target size requirements on constrained interfaces.

+ 56px above tab bar

FAB Placement mobile-005

Floating Action Button — a circular primary action button fixed at the bottom-right corner. Use for a single promoted action (compose, add, create). Limit to one per screen.

Implementation

Fixed position, `bottom: calc(16px + env(safe-area-inset-bottom))`, `right: 16px`. Size: 56px diameter. Place above the tab bar if present. Use `z-index` above content but below modals.

Accessibility

Must have an accessible label (`aria-label="Create new item"`). Should not obscure content — add scroll padding at the bottom. Consider hiding on scroll-down, showing on scroll-up.

font-size: 14px Email... iOS zooms! font-size: 16px Email... 48px height 16px prevents iOS auto-zoom

Input Field Sizing mobile-006

Text inputs need 48px minimum height and `font-size: 16px` to prevent iOS Safari from auto-zooming. iOS zooms the viewport when focused inputs have font-size below 16px, breaking layout.

Implementation

Set `min-height: 48px; font-size: 16px` on all `<input>`, `<textarea>`, and `<select>` elements. The 16px rule specifically prevents iOS zoom — use it even if your design system prefers smaller text.

Accessibility

Larger inputs benefit users with motor impairments. Clear visual focus styles (2px+ border change) required. Pair with visible labels above (not placeholder-only).

Navigation

Home Search Cart Profile 3-5 tabs

Bottom Tab Bar mobile-007

A fixed bar at the bottom of the screen with 3-5 navigation tabs, each showing an icon and label. The dominant mobile navigation pattern — used by every major app. More than 5 tabs becomes cramped.

Implementation

Fixed position at bottom with `padding-bottom: env(safe-area-inset-bottom)`. Each tab: icon + text label stacked vertically. Active tab highlighted with color and/or filled icon variant. Use `<nav>` with `role="tablist"`.

Accessibility

Use `<nav aria-label="Main navigation">`. Each tab: `role="tab"`, `aria-selected="true|false"`, `aria-current="page"` for active. Labels are mandatory — icon-only tabs fail WCAG 1.1.1.

× Home Products About Contact 80vw

Drawer Navigation mobile-008

A side panel (typically 80vw wide) that slides in from the left or right, containing navigation links. Best for apps with 5+ sections or deep hierarchies. Always closeable via X button, backdrop tap, and swipe.

Implementation

Width: `min(80vw, 360px)`. Close triggers: X button (top-right), backdrop click, swipe in closing direction. Transition: 250ms ease-out. Use `overscroll-behavior: contain` to prevent body scroll.

Accessibility

Focus trap inside open drawer. Close on Escape. Return focus to hamburger trigger on close. Drawer: `role="dialog"`, `aria-modal="true"`, `aria-label="Site navigation"`.

Details 44x44 ← swipe from edge

Back Navigation mobile-009

A left-pointing chevron or arrow in the top-left corner that navigates to the previous screen. The universal mobile "go back" gesture. Must align with both Android back button and iOS swipe-back expectations.

Implementation

Top-left position, min 44x44px touch target. Use `history.back()` or explicit route. On iOS, support swipe-from-left-edge with `touch-action: pan-x` on the first 20px of viewport.

Accessibility

Label: `aria-label="Go back"` or `aria-label="Back to [previous page]"`. The button itself must be a `<button>` or `<a>`, not a div with click handler.

Page Title Subtitle text here 56px scroll ↓ Page Title 44px More content visible

Collapsing Header mobile-010

A header that shrinks from a tall layout (56px with title/subtitle) to a compact bar (44px with title only) as the user scrolls down, maximizing content space. Restores when scrolling back up.

Implementation

Use `IntersectionObserver` on a sentinel element or `scroll` event (throttled) to toggle a `.collapsed` class. Transition: `height 200ms ease`. Hide subtitle with `opacity: 0` and `overflow: hidden`.

Accessibility

Content must remain accessible in both states. Never hide navigation controls during collapse. Use `prefers-reduced-motion` to disable the animation and snap between states.

Sheet Title Action 60%

Bottom Sheet mobile-011

A panel that slides up from the bottom to 40-60% of the screen height, with a drag handle for resize and swipe-down dismiss. Used for contextual options, filters, and secondary content without full navigation.

Implementation

Snap points at 40%, 60%, and 100% height. Drag handle: 32x4px rounded bar centered at top. Dismiss: swipe below 20% threshold. Always include visible close button — many users don't discover swipe dismissal.

Accessibility

Trap focus when covering >50% of viewport. `role="dialog"`, `aria-modal="true"`. Close button + Escape key. Announce opening: `aria-live="polite"`. Drag handle: `role="separator"` with `aria-label`.

Gestures

Delete List item swiped left ← swipe left = destructive Done List item swiped right swipe right = affirmative →

Swipe Left / Right mobile-012

Swipe horizontally on a list item to reveal actions — swipe left for destructive actions (delete, archive) in red, swipe right for affirmative actions (complete, favorite) in green. An 80px threshold prevents accidental triggers.

Implementation

Track `touchstart`/`touchmove`/`touchend`. Set `touch-action: pan-y` to allow vertical scroll while capturing horizontal swipe. Threshold: 80px horizontal displacement. Snap back if below threshold.

Accessibility

Always provide non-swipe alternatives (context menu, long-press, or visible buttons). Swipe is undiscoverable — include a tutorial on first use. Keyboard: Delete key or context menu.

64px threshold

Pull to Refresh mobile-013

Drag down from the top of a scrolled-to-top list to trigger a content refresh. Show a spinner animation pulled below the header. 64px pull threshold. The most recognized mobile gesture after tap and scroll.

Implementation

Detect `touchmove` when `scrollTop === 0`. Set `overscroll-behavior-y: contain` to prevent browser-native PTR. Show spinner when pulled past 64px. CSS: translate the content container down. Release triggers refresh.

Accessibility

Provide a "Refresh" button as alternative. Announce refresh state via `aria-live="polite"`: "Refreshing..." then "Content updated, N new items". Never use as the only way to get fresh content.

Pressed item 500ms hold Copy Share Delete

Long Press Context Menu mobile-014

A 500ms press-and-hold reveals a context menu of actions for the touched element. On mobile, render as a bottom sheet rather than a positioned dropdown to keep options in the thumb zone.

Implementation

Start timer on `touchstart`, cancel on `touchmove` (>10px displacement) or `touchend` before 500ms. Use `navigator.vibrate(10)` for haptic confirmation. Show bottom sheet with action list and Cancel button.

Accessibility

Long press is undiscoverable — always provide a visible menu trigger (kebab icon) as alternative. Context menu items must be keyboard-accessible. Announce menu opening via aria-live.

+ Always provide

Pinch to Zoom mobile-015

Two-finger pinch gesture to zoom into images, maps, or charts. Intuitive for visual content exploration. Always provide +/- button alternatives for single-finger and keyboard users.

Implementation

Use `touch-action: manipulation` on zoomable elements. Track two-touch distance changes. Apply CSS `transform: scale()`. Clamp zoom range (1x-5x). Double-tap to toggle between 1x and 2x.

Accessibility

Never disable `user-scalable=no` on the viewport — it prevents assistive technology zoom. Provide visible +/- buttons with aria-labels. Support keyboard zoom with + and - keys.

Item saved × velocity > 0.5px/ms → dismiss Notification ×

Swipe to Dismiss mobile-016

Swipe a toast, notification, or card horizontally to dismiss it. Uses velocity-based calculation — fast flicks dismiss even with small displacement. The standard dismissal gesture for transient UI.

Implementation

Track touch velocity (`distance / time`). Dismiss if velocity > 0.5px/ms OR displacement > 50% of element width. Animate out with `transform: translateX(100%)` + `opacity: 0`. Duration: 200ms.

Accessibility

Always include a visible close button (X) as alternative. Toasts must have `role="status"` or `role="alert"`. Auto-dismiss timer should be pausable on hover/focus (WCAG 2.2.1).

Tap 10ms = success 100-50-100ms = error pattern

Haptic Feedback mobile-017

Vibration feedback confirming touch interactions — 10ms for success/selection, 100-50-100ms pattern for error/warning. Enhances perceived responsiveness. Must be opt-out respecting user settings.

Implementation

`navigator.vibrate(10)` for tap confirmation. `navigator.vibrate([100, 50, 100])` for error pattern. Check `"vibrate" in navigator` before use. Respect `prefers-reduced-motion` — disable all haptics.

Accessibility

Haptics must supplement, never replace, visual and auditory feedback. Users with sensory disabilities may not perceive vibration. Always provide multi-modal feedback (visual + haptic + optional audio).

Layout

safe-area-top Safe content safe-area-bottom

Safe Area Insets mobile-018

CSS environment variables that account for hardware obstructions — the iPhone notch, Dynamic Island, Android camera cutouts, and home indicator bar. Without these, UI gets hidden behind device chrome.

Implementation

Add `<meta name="viewport" content="viewport-fit=cover">` to enable edge-to-edge. Then pad with `env(safe-area-inset-top)`, `env(safe-area-inset-bottom)`, `env(safe-area-inset-left)`, `env(safe-area-inset-right)`. Apply to fixed headers and bottom bars.

Accessibility

Ensures interactive elements are not obscured by device hardware. Critical for switch-access users who need to reach all interactive elements. Test on devices with notches and rounded corners.

example.com 100vh overflows! 100dvh fits perfectly

Dynamic Viewport Height mobile-019

Use `100dvh` instead of `100vh` for full-screen layouts. On mobile browsers, `100vh` includes the space behind the URL bar, causing content to overflow. `dvh` adjusts dynamically as the URL bar shows/hides.

Implementation

`height: 100dvh` for hero sections, modals, and full-screen interfaces. Fallback: `height: 100vh; height: 100dvh` for browsers without `dvh` support. Also available: `svh` (small) and `lvh` (large).

Accessibility

Prevents content from being hidden behind the mobile browser chrome, which particularly affects screen magnification users. Stable layout reduces cognitive load for all users.

Info (read) Input field Secondary Primary CTA Actions (tap)

One-Handed Reachability mobile-020

Arrange screen content in a reverse pyramid: critical interactive elements at the bottom (easy to reach), informational content at the top (readable but rarely tapped). Optimizes for one-handed phone use on larger screens.

Implementation

Place primary CTA buttons, input fields, and tab bars at the bottom. Move titles, descriptions, and status information to the top. Use `flex-direction: column-reverse` for action-first mobile layouts.

Accessibility

Benefits users with limited hand mobility. Ensure the visual order still makes logical reading sense — DOM order must match reading order even when visually rearranged. Use `order` CSS property cautiously.

14px font-size iOS zooms in! 16px font-size Email No zoom Never use user-scalable=no Fix: font-size ≥ 16px

Prevent Zoom on Input Focus mobile-021

iOS Safari auto-zooms the viewport when a focused input has `font-size` below 16px. This breaks layout and disorients users. The fix is CSS, not meta tags — never use `user-scalable=no`.

Implementation

Set `font-size: 16px` (or larger) on all `<input>`, `<select>`, and `<textarea>` elements. This is the only correct fix. Do NOT use `maximum-scale=1` or `user-scalable=no` in the viewport meta tag.

Accessibility

WCAG 1.4.4 (Resize Text) requires content to be zoomable to 200%. Disabling zoom via meta tags violates this. The 16px font-size approach prevents unwanted zoom while preserving intentional zoom capability.

Feedback

Story Content 5-7s auto-advance ← back next → hold = pause

Stories Format mobile-022

Full-screen vertical cards that auto-advance every 5-7 seconds. Tap right side to advance, left to go back, hold to pause. Progress indicators at the top show position. The dominant format for ephemeral mobile content.

Implementation

Full viewport height (`100dvh`). Progress bar: segmented `<div>`s with CSS animation (`width: 0 → 100%` over 5-7s). Pause on `touchstart`, resume on `touchend`. Preload next card's media for instant transitions.

Accessibility

`prefers-reduced-motion`: disable auto-advance, require manual tap. Provide pause/play button. Each card: `role="group"`, `aria-label="Story 3 of 8"`. Progress: `role="progressbar"`, `aria-valuenow`.

New message Tap to view 4s auto- dismiss ↑ swipe to dismiss

Pull-Down Banner mobile-023

A transient notification banner that slides down from the top of the screen, mimicking native push notification styling. Auto-dismisses after 4 seconds. Tappable to navigate to related content.

Implementation

Fixed position, `top: env(safe-area-inset-top)`. Animate in: `translateY(-100%) → translateY(0)` over 300ms. Auto-dismiss timer: 4s. Swipe up to dismiss manually. Queue multiple banners.

Accessibility

`role="status"` for informational, `role="alert"` for urgent. `aria-live="polite"`. Auto-dismiss must be pausable on hover/focus (WCAG 2.2.1). Provide dismiss button.

Heights match real content aspect-ratio locks layout

Mobile Skeleton Loading mobile-024

Placeholder shapes matching the real content layout, animated with a shimmer effect while loading. Heights must match actual content to prevent layout shift. More effective than spinners for perceived performance.

Implementation

Match skeleton dimensions to actual content using `aspect-ratio` to lock heights. CSS shimmer: `linear-gradient` animated with `@keyframes`. Max 3 shimmer cycles, then static gray. Use `<div aria-hidden="true">`.

Accessibility

Skeleton is `aria-hidden="true"`. Announce loading state separately: `<div role="status" aria-live="polite">Loading content...</div>`. Respect `prefers-reduced-motion` — show static gray, no shimmer.

Content

Order confirmed 2m ago Payment processed 5m ago Cart updated 1h ago CSS ::before

Timeline / Activity Feed mobile-025

A vertical sequence of timestamped events connected by a visual line, showing chronological activity (user actions, order updates, project milestones). Each entry has a node marker, timestamp, title, and optional detail.

Implementation

CSS vertical line via `::before` pseudo-element on the container (2px wide, centered). Node markers: 12px circles positioned over the line. Each entry: `<article>` with `<time datetime="...">`. Alternate left/right on desktop, stack on mobile.

Accessibility

Use `<ol>` or `<ul>` for the event list. `<time datetime="">` for machine-readable timestamps. `aria-label="Activity timeline"` on container. New events: `aria-live="polite"` region.

4 out of 5 stars 44px+ touch targets per star

Star Rating mobile-026

A 1-5 star rating input or display. For input: tap a star to set rating (tapping the same star toggles it off). Each star must be an individually tappable 44px+ target. Display ratings with filled/half/empty stars.

Implementation

Input: `role="radiogroup"` container with 5 `role="radio"` buttons. Use SVG star shapes. Display: `aria-label="Rated 4 out of 5 stars"`. Half-stars via SVG `clipPath`. Touch: min 44x44px per star.

Accessibility

Input: `role="radiogroup"` with `aria-label="Rating"`. Each star: `role="radio"`, `aria-checked`, `aria-label="1 star"` through `aria-label="5 stars"`. Arrow keys navigate. Display: `aria-label` with text value.

Desktop Details 2 Payment 3 Review 4 Confirm Mobile Step 2 of 4

Stepper / Wizard mobile-027

A multi-step process with a visual progress indicator showing completed, current, and upcoming steps. On mobile, the full stepper collapses to a compact "Step 2 of 5" label to save horizontal space.

Implementation

Desktop: horizontal step indicators with connecting lines. Mobile: `<span>Step 2 of 5</span>` with a thin progress bar. Each step: `<li>` in an `<ol>`. Completed steps are tappable to go back.

Accessibility

`<nav aria-label="Progress">` containing `<ol>`. Current step: `aria-current="step"`. Completed steps: links. Future steps: `aria-disabled="true"`. Announce step changes via `aria-live`.

System Light Dark prefers-color-scheme + localStorage

Dark Mode Toggle mobile-028

A three-state toggle (System / Light / Dark) that respects `prefers-color-scheme` by default and saves user overrides to localStorage. System option follows OS preference and updates live.

Implementation

Check `prefers-color-scheme` media query for default. Save override to localStorage. Apply via `data-theme` attribute on `<html>`. Use CSS custom properties for all colors. Transition: 200ms on `background-color` and `color`.

Accessibility

Use `role="radiogroup"` for three-state toggle. Each option: `role="radio"`, `aria-checked`. Ensure all color combinations meet WCAG contrast in both modes. Never rely on color alone for information.

Shows after 400px Intersection Observer

Back to Top Button mobile-029

A button that appears after scrolling 400-600px, smoothly scrolling to the top of the page when tapped. Essential for long-scrolling mobile pages where reaching the header requires excessive thumb effort.

Implementation

Show/hide with `IntersectionObserver` on a sentinel element at 400-600px from top. `window.scrollTo({ top: 0, behavior: "smooth" })`. Position: bottom-right, above FAB if present. `prefers-reduced-motion`: use `behavior: "instant"`.

Accessibility

`aria-label="Back to top"`. Move focus to the top of the page (e.g., skip-nav target or `<main>`) after scrolling. Hidden when at top: `aria-hidden="true"` or `display: none` to remove from tab order.

Utilities

📋 Copy link ✉ Email 💬 Message Cancel navigator .share()

Share Sheet / Action Sheet mobile-030

A bottom-mounted list of share targets or actions triggered by a share button. Uses the native Web Share API when available for OS-level sharing, with a custom fallback bottom sheet listing copy-link, email, and social options.

Implementation

Check `navigator.share` support. If available: `navigator.share({ title, text, url })`. Fallback: custom bottom sheet with share options. Always include "Copy link" as first option. Cancel button at the bottom.

Accessibility

Share trigger: `aria-label="Share this [item]"`. Bottom sheet: `role="dialog"`, `aria-modal="true"`, `aria-label="Share options"`. Focus trap. Each option: `role="button"` or `<button>`. Cancel returns focus.

Card Content Swipe to decide keyboard alt

Swipeable Card Stack mobile-031

A deck of cards where the top card can be swiped left (reject) or right (accept) — the Tinder-style interaction pattern. Cards behind are slightly scaled down to create depth. Used for decision-making interfaces.

Implementation

Stack with `position: absolute`. Top card tracks touch with `transform: translateX() rotate()`. Rotation: `angle = displacement * 0.1deg`. Behind cards: `scale(0.95)`, `translateY(10px)`. Animate exit: fly off screen 300ms.

Accessibility

Provide arrow buttons (left/right) below the stack as keyboard alternatives. Arrow keys for navigation. Each card: `role="group"`, `aria-label`. Announce card content when it becomes the top card.

Item 1 Item 2 Item 3 scroll-snap-type: x mandatory

Horizontal Scroll with Snap mobile-032

A horizontally scrolling container where items snap to alignment points, creating a carousel-like experience without complex JavaScript. Used for image galleries, product rows, and category browsing.

Implementation

`scroll-snap-type: x mandatory` on container. `scroll-snap-align: start` on children. `overflow-x: auto; -webkit-overflow-scrolling: touch`. Hide scrollbar: `scrollbar-width: none`. Add gradient edge fades for affordance.

Accessibility

`role="region"` with `aria-label` and `tabindex="0"` for keyboard scrollability. Arrow keys scroll. Provide visible prev/next buttons. Ensure all items are reachable — do not use `overflow: hidden` which hides content.