RamosLabs DS

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.

Part 1The map, every desktop pattern has a mobile home
1

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.

Each desktop pattern and its mobile replacement, with the anchoring source.
Desktop patternMobile replacementAnchor source
Centered modal / dialog toBottom 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 toAction 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 toTap to reveal, inline disclosure, or tap to a sheetThere is no reliable :hover on pointer: coarse (Section 8).
Right click (context menu) toLong press, plus a visible overflow controlWCAG 2.5.1 lists "long press" as a valid single-pointer alternative.
Persistent left sidebar toBottom navigation bar (3 to 5) or a drawerMaterial: the navigation bar "lets people switch between UI views on smaller devices."
Hover to reveal actions toNo such state. Make actions visible or reveal on tappointer: coarse does not fire hover (Section 8).
Top toolbar of actions toBottom app bar or a sticky bottom CTAThumb zone, Section 3.
Toast, top of the screen toSnackbar anchored to the bottomMaterial: "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.

Part 2The components, anchored to the bottom
2

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.

Open with 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".
Send focus to Cancel, not Confirm. On open, focus lands on the first focusable element; put autofocus on the cancel or close control so that "confirmation is deliberate and not accidental."
Escape is free. A dialog opened with showModal() "can be dismissed by pressing the Esc key." You do not wire it.
Give it an explicit close. MDN: "The most robust way to ensure that all users can close the dialog is to include an explicit button." The swipe and the grabber can never be the only way out.
Return focus on close. Native <dialog> restores focus to the trigger in most cases; with custom animation you restore it yourself.
Anchor to the bottom with CSS, not a new component. Set 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.
The drag handle is a real control. In Material the handle "will automatically receive and handle accessibility commands to expand and collapse." On web that means a genuine <button> the keyboard can operate to cycle snap points, sized to the 48px touch-target floor, not a decorative bar you can only drag.
Live: a real modal bottom sheet, native <dialog> anchored to the bottom
9:41
Ticket options
General admission

Transfer this ticket

Send the ticket to another attendee by email. They confirm it on their own device. You keep a record of the transfer.

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.

Bottom sheet when

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.

Centered modal when

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.

Modals, the full dialog contract
3

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.

Live: content scrolls, the primary CTA stays pinned in the thumb band
9:41
Checkout
2 x General admission

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.

4

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.

Live: bottom nav with aria-current on the active tab, plus a single FAB
9:41
Events
Design Systems Summit
Payments Deep Dive

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.

5

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).

Live: a bottom snackbar with a single Undo action, role="status"
9:41
Saved events
Payments Deep Dive
Event removed from saved.

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.

Part 3Touch rules, gestures, safe areas, activation
6

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.

Live: a swipe-to-delete row that also exposes its action as a visible button
  • Mariana RestrepoGeneral admission, order 4521
  • Jordan EllisVIP, order 4520

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.

7

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">.

/* MDN canonical sticky bottom bar */
footer {
  position: sticky;
  bottom: 0;
  padding: 1em 1em calc(1em + env(safe-area-inset-bottom, 0px));
}

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.

8

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.

/* Capability, not user agent */
@media (pointer: coarse) {
  /* larger targets, touch patterns, no hover reveal */
}
@media (pointer: fine) {
  /* mouse density, hover affordances */
}
/* Switch container by capability OR width */
@media (pointer: coarse), (max-width: 640px) { /* sheet, bottom nav */ }

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.

pointer: coarsewidth breakpointany-hover: none

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). CSS env() and safe-area insets with the sticky footer pattern (Baseline 2020). The pointer media 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.