Patterns / Mobile First
The same content does not deserve the same container.
A modal is centered on a desktop because a cursor can reach the middle of a wide screen without effort. A thumb on a 375px phone cannot. So the mobile equivalent of a centered modal is a bottom sheet anchored to the bottom edge, where the thumb already rests. This page is the last word on how we relocate desktop patterns downward, into the reachable zone, anchored to the safe area, and how we keep every one of them operable by keyboard and announced correctly. It is not a coat of paint on the desktop layout. It is a different container for the same content.
The desktop to mobile map
Patterns that lean on a cursor, on hover, and on a wide screen (a centered modal, a hover menu, a tooltip, a right click, a persistent sidebar) are ergonomically hostile to a thumb. Steven Hoober's field study of 1,333 observations found that 49% of people use a phone one-handed, with the thumb as the dominant actuator, and that grip changes "very often, sometimes every few seconds." The comfortable arc of the thumb covers the lower half of the screen; the top edge forces a regrip. That single fact drives the whole map: primary actions and navigation move down, anchored to the bottom edge, inside the safe area.
| Desktop pattern | Mobile replacement | Anchor source |
|---|---|---|
| Centered modal / dialog to | Bottom sheet (dialog anchored to the bottom edge) | web.dev: at a small viewport the bottom margin goes to 0 and the mega dialog "transforms into an action sheet." |
| Hover menu / dropdown to | Action sheet of options (a list-only bottom sheet) | Material modal bottom sheet: "present a set of choices while blocking interaction with the rest of the screen." |
| Hover tooltip to | Tap to reveal, inline disclosure, or tap to a sheet | There is no reliable :hover on pointer: coarse (Section 8). |
| Right click (context menu) to | Long press, plus a visible overflow control | WCAG 2.5.1 lists "long press" as a valid single-pointer alternative. |
| Persistent left sidebar to | Bottom navigation bar (3 to 5) or a drawer | Material: the navigation bar "lets people switch between UI views on smaller devices." |
| Hover to reveal actions to | No such state. Make actions visible or reveal on tap | pointer: coarse does not fire hover (Section 8). |
| Top toolbar of actions to | Bottom app bar or a sticky bottom CTA | Thumb zone, Section 3. |
| Toast, top of the screen to | Snackbar anchored to the bottom | Material: "brief messages ... at the bottom of the screen." |
How the map is switched on: by input capability, never by user agent. Detect @media (pointer: coarse) and viewport width. A touchscreen laptop, an iPad with a trackpad, a phone in a desktop dock: none of these fit a substring match on navigator.userAgent, and all of them break it. The switch is a capability query, combined with a width breakpoint. Section 8 has the mechanics.
Bottom sheet, the modal's mobile form
Material draws two variants. A standard bottom sheet "co-exists with the screen's main UI region and allows for simultaneously viewing and interacting with both regions," so it carries no scrim, good for secondary content that stays on screen while the main area scrolls. A modal bottom sheet "present[s] a set of choices while blocking interaction with the rest of the screen," sits above a scrim, and is the direct replacement for a centered modal and for inline menus. iOS expresses the same idea as a sheet with resizable detents (medium and large snap points) and an optional grabber, treated here as an iOS convention rather than a verbatim rule (see Sources).
A bottom sheet is still a dialog. It is not a decorated <div>. Build the modal variant on the native <dialog> element and let the platform hand you most of the accessibility for free.
showModal(). MDN: it "block[s] interaction with other UI elements, making the rest of the page inert." The browser applies inert to the rest of the document for you, and implies aria-modal="true".autofocus on the cancel or close control so that "confirmation is deliberate and not accidental."showModal() "can be dismissed by pressing the Esc key." You do not wire it.<dialog> restores focus to the trigger in most cases; with custom animation you restore it yourself.inset: auto 0 0 0 so the sheet sits on the bottom edge, style the ::backdrop for the scrim, and pad the bottom with env(safe-area-inset-bottom) so content clears the home indicator.<button> the keyboard can operate to cycle snap points, sized to the 48px touch-target floor, not a decorative bar you can only drag.Standard: modal bottom sheet
Try Open it, then press Escape, or Tab to Cancel (which holds focus on open) and press Enter. Both close it. The backdrop dims the page and the rest goes inert.
Bottom sheet or centered modal? Same component, one media query.
The viewport is small (pointer: coarse or width at or below the breakpoint), the task is scoped and tied to context, or it is a list of actions or a short form where thumb reach matters. The standard variant when the user needs to keep seeing the content behind.
There is a cursor (desktop), or the dialog is genuinely blocking and critical (a destructive confirmation with forced focus), or content does not sit comfortably against the bottom edge.
Do not duplicate the component. It is the same <dialog> in both cases. Only the positioning changes by media query: centered on wide screens, anchored to the bottom on narrow ones. One accessibility contract, two layouts.
Thumb zone and the sticky bottom CTA
Hoober's field data is the anchor here. One-handed use: 49% (67% right thumb, 33% left). Cradling: 36%. Two-handed: 15%, and 90% of that in portrait. When cradling, the thumb is on the screen 72% of the time. And, verbatim: users "change the way they're holding their phone very often, sometimes every few seconds." The thumb owns the lower band; the top edge is the stretch zone. So the primary action lives at the bottom, pinned (sticky), full width or aligned to the dominant thumb.
Target size: the legal floor is not the goal. WCAG 2.5.8 (AA) sets the minimum at "24 by 24 CSS pixels," but that is a floor with exceptions, not a target. NN/g recommends far more: interactive elements "at least 1cm by 1cm," because "the impact area of the typical thumb is an average of 2.5cm wide," and targets must be big enough and then also spaced well enough. Our rule for anything in the thumb zone: at least 44 to 48px tall, in line with Material's 48dp and NN/g's 1cm. Never the bare 24px for a primary action.
Rule The primary action is a full-width control pinned to the bottom, at least 48px tall, in the thumb band.
Why Hoober: the thumb comfortably covers the lower half, and grip shifts constantly. A bottom-pinned action is reachable regardless of the current grip.
Rule Pad the sticky bar's bottom with calc(base + env(safe-area-inset-bottom)).
Why MDN gives this exact pattern so "fixed app toolbar buttons are not obscured" by the home indicator. Section 7 covers the insets.
Note The tinted arc marks the comfortable thumb reach from a bottom-right grip. The CTA sits inside it; the app title sits in the stretch zone, where only navigation labels belong.
Bottom navigation and the FAB
Material: a navigation bar "lets people switch between UI views on smaller devices" and holds three to five destinations (the component "does not support more than 5"). Each destination is a link or button with an accessible name, and the active one carries aria-current="page". Use a bottom bar for three to five top-level destinations of equal weight that stay visible; use a drawer when there are more than five or the navigation is secondary or hierarchical. On desktop that same set becomes the persistent sidebar.
A FAB "represents the primary action of a screen," a single, unambiguous action like create or compose. Use it only when there is exactly one such action. Do not pair a FAB with a sticky bottom CTA: they compete for the same thumb band, so on any one screen they are usually mutually exclusive.
Standard: bottom navigation and FAB
Note The active tab is marked with aria-current="page", so a screen reader announces it as current, not by color alone. The FAB carries an aria-label because its glyph has no visible text.
Snackbar, feedback in the thumb band
Material: snackbars "provide brief messages about app processes at the bottom of the screen" and "shouldn't interrupt the user experience." Only one shows at a time (a new one dismisses any previous), it may carry a single optional action such as undo or retry, and it is "the preferred mechanism for displaying feedback messages," with toast reserved for when a snackbar cannot be used. It replaces the desktop toast that lived in the top-right corner. On web: a region with role="status" (or aria-live="polite") anchored to the bottom with env(safe-area-inset-bottom); if it has an action, that action is a real <button> reachable by keyboard, and never dependent on a swipe (Section 6).
Standard: bottom snackbar
Note The message sits in a polite live region so it is announced without stealing focus. The Undo action is a keyboard-reachable button, the same action a swipe would have triggered.
Gestures, and the rule that never bends
Swipe to delete, long press for a context menu, pull to refresh, drag on a sheet's snap points: all useful, all mobile-native. The non-negotiable rule: a gesture can never be the only path. There must always be an alternative that works with a single pointer and with a keyboard.
Rule Any path-based or multipoint gesture has a single-pointer alternative.
Why WCAG 2.5.1 Pointer Gestures (Level A): functionality using such gestures "can be operated with a single pointer without a path-based gesture" unless essential. A tap, click, or long press qualifies. Swipe to delete needs a visible Delete button.
Rule Any dragging movement has a no-drag alternative.
Why WCAG 2.5.7 Dragging Movements (2.2, Level AA): achievable "by a single pointer without dragging," for example tapping buttons to move items or picking from a pop-up menu. The sheet's drag handle needs buttons or taps that expand, collapse, and close it.
Rule Long press replaces right click only alongside a visible overflow control.
Why Long press is neither discoverable nor keyboard-operable. The same action must live in a visible "more options" menu so every user can reach it.
Rule Custom pull to refresh ships with a visible Refresh button.
Why Native browser scroll and the OS pull-to-refresh fall under the user-agent exception, but a gesture your own content interprets does not. If you build it, give the same action an explicit control.
- Mariana Restrepo
- Jordan Ellis
Note Swiping is the shortcut, not the requirement. The Remove button carries the same action, is at least 44px tall, and a keyboard can reach it. It is styled in the brand indigo, not an alarm color, because destructive intent is carried by the label and the confirmation step, not by hue.
Safe-area insets, notch and home indicator
MDN defines env(safe-area-inset-*) as "the safe distance from the top, right, bottom, or left inset edge of the viewport, defining where it is safe to place content without risking it being cut off by the shape of a non-rectangular display." The four variables are safe-area-inset-top, -right, -bottom, and -left. They are 0 on a plain rectangular viewport and larger than zero around a notch or the home indicator, but only when the page opts in with <meta name="viewport" content="viewport-fit=cover">.
Rule Never pad a fixed edge with a bare constant. Use calc(base + env(safe-area-inset-*, 0px)).
Why The calc() keeps your normal spacing on rectangular screens and adds only the device inset where one exists. The 0px fallback keeps it correct on browsers that do not supply the variable. env() has been widely available since January 2020.
Out of scope, not yet normative: the virtual keyboard. Handling the on-screen keyboard resizing the viewport (the VirtualKeyboard API, interactive-widget, dynamic viewport units) is a live and shifting area we have not verified against a primary source for this page. iOS documents automatic keyboard avoidance for sheets as a platform behavior, but the web story is experimental. Treat it as a follow-up, not a rule, and confirm current support before relying on it.
Enable the patterns on touch, without sniffing
Do not read navigator.userAgent. Detect the capability of the input and the size of the viewport, because that is what actually decides whether these patterns fit. MDN's pointer media feature tests the primary pointer's accuracy: coarse is "a pointing device of limited accuracy, such as a finger on a touchscreen," fine is "an accurate pointing device, such as a mouse," and none is no pointer at all. Use pointer for the primary device and any-pointer for any device present.
Rule On pointer: coarse, assume no reliable hover. Reveal-on-hover patterns do not exist; make actions visible or reveal on tap.
Why A touchscreen has no persistent hover state. Anything hidden behind :hover is simply unreachable by a finger, which is why the map has no mobile home for "hover to reveal."
Rule Switch the container on pointer: coarse OR width at or below the breakpoint, so both phones and touch tablets are covered.
Why Width alone misses a small touch tablet; capability alone misses a narrow mouse window. The union of the two is what reliably picks the sheet-and-bottom-nav layout. pointer has been widely available since December 2018.
Why These compact chips are the whole detection surface: primary pointer accuracy, viewport width, and hover availability. No user-agent string appears anywhere in it.
Sources
- Steven Hoober, How Do Users Really Hold Mobile Devices? (UXmatters, 2013). Field study of 1,333 observations: 49% one-handed (67% right thumb), 36% cradling, 15% two-handed; thumb on screen 72% while cradling; grip changes very often, sometimes every few seconds. The basis for anchoring actions at the bottom. uxmatters.com/mt/archives/2013/02
- Aurora Harley, Touch Targets on Touchscreens (Nielsen Norman Group, 2019). Interactive elements at least 1cm by 1cm; the typical thumb impact area averages 2.5cm; targets must be big enough and then spaced well enough. nngroup.com/articles/touch-target-size
- W3C, Understanding WCAG. SC 2.5.1 Pointer Gestures (A), single-pointer alternative for path-based and multipoint gestures. SC 2.5.7 Dragging Movements (2.2, AA), no-drag alternative with the user-agent exception. SC 2.5.8 Target Size Minimum (2.2, AA), 24 by 24 CSS px floor with exceptions. w3.org/WAI/WCAG22/Understanding
- MDN Web Docs. The
<dialog>element (showModal()makes the rest inert, Escape closes, focus behavior,aria-modal,::backdrop, explicit close button). CSSenv()and safe-area insets with the sticky footer pattern (Baseline 2020). Thepointermedia feature, coarse and fine and none (Baseline 2018). developer.mozilla.org - Adam Argyle, Building a dialog component (web.dev). An accessible native dialog, focus placed on the cancel action so confirmation is deliberate, and the mega dialog transforming into a bottom-anchored action sheet at small viewports. web.dev/building-a-dialog-component
- Material Components (official docs, GitHub material-components-android). Bottom Sheet (standard vs modal, scrim, the 48dp accessible drag handle that handles expand and collapse commands). Snackbar (bottom, one at a time, one optional action, preferred over toast, readable by screen readers). Floating Action Button (single primary action, sizes, content description). Bottom Navigation (three to five destinations, a title per item). github.com/material-components/material-components-android
- Apple Human Interface Guidelines, Sheets and Action Sheets. Sheets for a scoped task tied to context; resizable detents (medium and large); the grabber as a resize hint; nonmodal sheets; action sheets anchored at the bottom on iPhone. Referenced as iOS conventions. iOS convention, cited via snippet, not verified verbatim developer.apple.com/design/human-interface-guidelines
- House rules layered on the cited guidance, not verbatim claims: the desktop-to-mobile mapping table and the "same component, one media query" position (design judgment anchored in the sources above), the 44 to 48px thumb-zone floor (reconciling Material 48dp, NN/g 1cm, and the WCAG 24px minimum), the FAB and sticky-CTA mutual-exclusion guidance, and the capability-OR-width activation rule. The virtual keyboard subsection is explicitly marked out of scope and not yet verified against a primary source.