Skip to main content
ANVISoftware Solutions
Lesson 2 of 11Intermediate17 min

Mobile UI

By the end of this lesson

Design for touch, small screens and varying densities.

The input device for a mobile app is a hand. That sentence does more work than it looks like it does, because almost every difference in this lesson follows from it.

A mouse pointer has a precise position, a hover state that previews what will happen, and a second button for secondary actions. A finger has none of those. It covers an area rather than a point, it obscures the thing it is touching, and the tap is the whole interaction — press and result, with nothing in between.

Field conditions make it harder still. An engineer may be standing, holding a device in one hand, in bright sunlight, possibly wearing gloves, and looking away every few seconds. A layout that tests well on a desk at arm's length can be genuinely unusable there.

Four terms used throughout the rest of this lesson:

Touch target
The area that responds to a tap. It is not the same as the drawn control: a 16-unit icon can sit inside a 48-unit target, and usually should. Users judge a control by where it appears, so the visible thing can be small if the tappable box around it is not.
Density-independent unit
A logical unit of size that the platform multiplies by the screen's pixel density before drawing. Two devices with very different pixel counts render the same number as roughly the same physical size. iOS calls them points, Android calls them dp, and the idea is identical. Physical pixels are the wrong unit for anything you type by hand.
Safe area
The part of the screen not covered by hardware or system interface: notches, camera cutouts, rounded corners, the status bar, and the home indicator at the bottom. The platform tells you its size at runtime, because it differs per device and changes on rotation.
Dynamic type
The text size the user chose in their device settings. It applies system-wide, and your app either honours it or overrides it. Honouring it is the highest-value accessibility decision available to you on mobile.

Sizing rules worth treating as fixed constraints rather than suggestions. The figures are in density-independent units:

  • Minimum touch target of roughly 44 on iOS and 48 on Android. Using 48 everywhere satisfies both and saves an argument
  • At least 8 units of clear space between neighbouring targets, and considerably more when one of them is destructive
  • Grow the target rather than the icon. Padding around a small glyph is how you reach the minimum without a screen full of oversized buttons
  • Keep around 16 units of inset from the screen edges for tappable content, since a thumb wrapping the edge of the device produces unreliable touches
  • A row in a list is a target. Make the whole row tappable rather than only the text inside it
  • Never place two targets closer than the width of a fingertip when one of them deletes something
  • Express every one of these numbers in density-independent units. A height in physical pixels is correct on exactly one device
Sizing rules expressed once, in code
TypeScript
/** Density-independent units. The platform scales them for the screen. */
export const touch = {
  minTarget: 48,
  minGap: 8,
  edgeInset: 16,
  destructiveGap: 24,
} as const;

interface TargetBox {
  minWidth: number;
  minHeight: number;
  padding: number;
}

/** Pads a small glyph out to a thumb-sized tappable box. */
export function targetFor(visualSize: number): TargetBox {
  const padding = Math.max(0, (touch.minTarget - visualSize) / 2);
  return { minWidth: touch.minTarget, minHeight: touch.minTarget, padding };
}

/** Honours the user's text size setting, with an upper guard. */
export function scaledText(baseSize: number, userScale: number): number {
  const applied = Math.min(Math.max(userScale, 1), 2);
  return Math.round(baseSize * applied);
}

const editIcon = targetFor(16); // 16-unit glyph, 48-unit target
const bodyText = scaledText(16, 1.6); // 26 at the user's setting
  • Treat this as a shape rather than an API. Whatever toolkit you use has its own way to express sizes; the point is that the numbers live in one module instead of being retyped per screen, so a review can check them in one place.
  • targetFor separates what is drawn from what is tappable. A 16-unit pencil icon with 16 units of padding on each side gives a 48-unit box, which is why an icon-only button can look restrained and still be comfortable to hit.
  • scaledText multiplies by the user's setting instead of ignoring it. That multiplication is the whole feature: a hard-coded 16 stays at 16 no matter what the user asked for, which quietly overrides an accessibility choice they made deliberately.
  • The clamp at 2 is a real trade-off, stated honestly. Unbounded growth breaks dense layouts, and a guard keeps them usable. But every unit you shave off that ceiling is taken from someone who needs large text, so a cap of 1.2 is not a compromise, it is a refusal. Prefer fixing the layout to lowering the cap.
  • Percentages and fractions of the screen are fine for widths. They are the wrong tool for touch targets, because a fraction of a small screen is a small target.

Reach, safe areas and the user's text size

Hold a phone in one hand and the thumb sweeps an arc. The bottom centre of the screen is comfortable, the middle is reachable, and the far top corner requires shuffling the device in your palm or using the other hand. Screens have grown considerably; thumbs have not. So put the primary action low and within that arc, and reserve the top corners for things used rarely — a settings entry point, a close button. A save button in the top right corner is a small tax charged every time, and one-handed users pay it most.

Reach also argues for keeping destructive actions away from frequent ones. If "add note" and "delete visit" sit side by side in the reachable zone, a mistap is a matter of time. Separate them, and require a confirmation for the one that cannot be undone.

Safe areas are the other geometry problem. The screen is not a rectangle you fully control: a cutout may take a bite from the top, corners are rounded, and at the bottom sits a home indicator that swallows upward swipes. Ask the platform for the safe area insets at runtime and lay out inside them rather than hard-coding numbers per device, which ages badly with every new handset. The on-screen keyboard is the same problem in motion — it can cover half the screen, so a form has to scroll the focused field into view rather than trusting it to be visible.

Then there is text size, and this is where most mobile accessibility is won or lost. Many people run their device above the default, some well above, and the reasons range from low vision to being forty-five. If your app honours the setting, they can read it. If it does not, your app is the one they squint at. Honouring it means text that grows, containers that grow with the text, and layouts that reflow rather than clip. Test at the largest setting you support and fix what breaks — that testing pass finds more real accessibility problems per hour than anything else you can do on mobile.

Two things travel with dynamic type. Contrast, because sunlight on a screen turns light grey text into no text at all, and a ratio that passes on a monitor can fail outdoors. And labels, because an icon-only button is silent to a screen reader unless you give it an accessible name. A pencil glyph with no name is announced as "button", which tells the user nothing about what tapping it does.

Summary

  • A fingertip covers an area and has no hover, so targets need roughly 48 density-independent units, reached by padding a small icon rather than enlarging it
  • Size everything in density-independent units; physical pixels are correct on exactly one device
  • Put primary actions within one-handed thumb reach and keep destructive actions away from frequent ones
  • Read safe area insets at runtime instead of hard-coding device geometry, and handle the keyboard covering the layout
  • Honouring the user's text size setting is the highest-value accessibility decision on mobile, and testing at the largest size is how you keep it working

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Audit one screen against the numbers

Take any screen from an app you use daily and measure it by eye against this lesson: are icon-only buttons comfortably tappable, is the primary action within thumb reach, and does anything sit close to the top corners?

Now turn your device's text size up to its largest setting and open the same screen. Write down every place text is clipped, truncated or overlapping.

Show solution

Most apps pass the touch target test and fail the text size test, usually in the same two places: tab bar labels and anything inside a fixed-height row.

The reason is worth understanding, because it will happen to your app too. Touch targets are checked once during design and then stay correct. Dynamic type breaks later, when someone adds a row with a fixed height because it looked tidy at the default size. Nothing fails in review, no test goes red, and the damage lands only on users who changed the setting.

That makes a run at the largest supported text size a fixed step before release, not a one-off audit. It is the cheapest accessibility check available on mobile and it catches real breakage every time.

Think about it

Place the controls on a visit note screen

The visit note screen in the field app shows the employee's name and site, a multi-line notes field, a Save action, an Attach photo action, and a Discard action.

Decide where each control goes and why. Assume the engineer is standing, using one hand, and may be interrupted at any moment.

Show solution

Save belongs low and prominent, inside the thumb arc. It is the action taken every time, and a top-right Save is reached awkwardly on every single visit.

Attach photo sits near the notes field it relates to, at full touch target size, because proximity is what tells the user the photo attaches to this note rather than to the visit record in general.

Discard does not belong next to Save. Put it behind a menu or at the top, and confirm it. The cost of a mistaken Discard is an engineer retyping notes while standing in a plant room, and the cost of one extra tap is nothing by comparison.

There is a stronger answer than careful Discard placement, which is to make it unnecessary: save the draft continuously so an interruption costs nothing. That is the subject of the state lesson, and it is a good illustration that some UI problems are better solved by changing the behaviour than by moving the button.

One defensible variation: on a screen where saving is the only sensible outcome, drop the explicit Save entirely and persist as the user types. Fewer controls, less to reach for. It requires clear feedback that the note is saved, or users will not believe it.

Saved in this browser only.