My Hiro prototype matched Figma pixel for pixel. It still felt wrong the second I tapped it.

It looked like an iPhone app and behaved like a web page.

What fixed it was behavior: the small rules iOS follows that nobody puts in a Figma file. Here's the list I added, with the real numbers.

One thing up front: real UIKit can't run in a browser. This is HTML and CSS copying how iOS behaves. But copied closely enough, your thumb stops noticing.


TL;DR

  • Every tap target is at least 44 × 44pt, even when the icon is 24px.
  • Highlight on touch-down, act on touch-up. Slide off the button and it cancels.
  • Highlights appear instantly and fade out. Never the other way around.
  • Sheets follow your finger and dismiss past 110pt or on a quick flick.
  • Swipe from the left edge to go back.
  • The keyboard is the hard part. Callouts, caps lock, hold-to-delete, and a "From Messages" code.

Build it at the real size first

The Hiro frames are 375 × 812, an iPhone at 1x. The prototype renders at exactly that size and scales down to fit the page, so every measurement matches the design. (I wrote about that setup in Turn a Figma File Into a Live Prototype.)

That matters here because iOS rules are in points. At native size, 44pt in the guidelines is 44px in the code. No converting.


1. 44pt tap targets

Apple's minimum tap target is 44 × 44pt. Lots of iOS icons are drawn smaller than that: a 24px clear button, a 30px close button.

You don't make the icon bigger. You make the hit area bigger with an invisible pseudo-element:

/* 44x44pt minimum hit area (HIG) without changing the drawn size.
   The element must be positioned (relative or absolute). */
.hit::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: max(100%, 44px);
  height: max(100%, 44px);
  translate: -50% -50%;
}

The icon looks exactly like Figma. Your thumb gets a real target.


2. Press states that act like UIKit

On iOS, different controls react differently when you touch them:

ControlWhat happensRelease
Filled buttonDims, brightness(0.78)220ms
Plain buttonFades, opacity: 0.35260ms
List rowGray cell highlight320ms
Keyboard keyLights up160ms

The important part: the highlight is instant. Every pressed state has transition-duration: 0ms. Only the release animates.

A lot of web buttons do the opposite. They ease into the pressed state, and that little delay is exactly what makes a tap feel slow.


3. Highlight on touch-down, act on touch-up

iOS buttons light up the moment your finger lands. But the action only fires when you lift your finger, and only if you're still on the button.

So the prototype sets a data-pressed attribute on pointerdown and runs the action on click, which is touch-up inside.

And if your finger slides off, the highlight cancels:

// Sliding off a control cancels its highlight, as on iOS
if (el && !el.contains(document.elementFromPoint(e.clientX, e.clientY))) {
  el.removeAttribute("data-pressed");
  pressed.current = null;
}

On Velta, scrolling a list also cancels the highlight on the row under your finger. Otherwise every scroll looks like a tap.

The one exception is the delete key. It acts on touch-down, because it repeats while you hold it.


4. Sheets that follow your finger

A sheet that just slides away when you tap outside feels like a web modal. On iOS you grab it and pull it down.

The rules:

  • The grabber is 36 × 5pt, radius 2.5, at 30% gray.
  • Only the top 58pt starts a drag, so you can still tap buttons inside the sheet.
  • Dismiss past 110pt, or on a flick faster than 0.6pt per ms.
  • Pull up and it rubber-bands at 0.15x your finger.
if ((e.clientY - top) / scale > 58) return;            // only the top 58pt starts a drag
setSheetDrag({ dy: d.dy < 0 ? d.dy * 0.15 : d.dy });   // upward drag rubber-bands
if (sheetDrag.dy > 110 || g.vy > 0.6) return closeSheet();

Then the detail that sells it. When a page sheet opens, the screen behind shrinks back into a card:

/* Screen behind a page sheet shrinks to the 10px card edge above it */
.stackBehind {
  scale: 0.9147;
  translate: 0 58px;
  border-radius: 10px;
  filter: brightness(0.7);
}

And while you drag the sheet down, the screen behind grows back in step with your finger. Let go past the threshold and it finishes the move. Let go early and it springs back.


5. Swipe from the edge to go back

Every iOS user does this without thinking. A web prototype that ignores it feels broken.

  • Start within 24pt of the left edge.
  • Go back past 35% of the screen width, or on a flick faster than 0.45pt per ms.
  • The previous screen slides in from -30% and brightens as it comes.
/* Interactive pop: drag from the left edge, like UINavigationController */
if (!canSwipeBack || e.button !== 0 || local(e).x > 24) return;
const commit = swipe.p > 0.35 || g.vx > 0.45;

It's turned off where iOS turns it off: on the home screen, while a sheet is open, and in the middle of a transition.


6. The keyboard

This took the longest. You don't notice a real iOS keyboard until a fake one gets something wrong.

What's in the prototype:

  • Key callouts. Press a letter and an enlarged key pops up above your finger, instead of the key just changing color.
  • Shift and caps lock. Tap shift once for one capital. Double-tap within 350ms to lock caps. There's a small bar under the arrow when it's locked.
  • Auto capitals. Shift turns back on after a space and when the field is empty.
  • Layers. Letters, then 123, then #+=, like the real one.
  • Hold to delete. The first repeat comes after 450ms, then one every 80ms.
  • QuickType bar. The first suggestion is the word you typed, in quotes.
  • "From Messages". On the verification screen, the one-time code shows up above the keyboard. Tap it and it fills in.

That last one is a real iOS feature most people use every day without thinking about it.


7. The small stuff

  • The search bar. Focus it and a Cancel button slides in. Start typing and the mic becomes a clear button.
  • The loading spinner. iOS doesn't use a spinning circle. It's 8 spokes that fade around, stepping in steps(8) over 800ms.
  • The curve. Pushes and sheets use cubic-bezier(0.32, 0.72, 0, 1). Push takes 500ms, sheet in 500ms, sheet out 320ms.
  • Icons. SF Symbols can't be exported from Figma, so shift, delete, mic, search, and close are drawn inline as SVG.

Web problems iOS doesn't have

A few things only break because the phone lives inside a web page:

  • Typing needs focus on the phone. Taps keep focus on the phone itself, not the tapped button, so the keyboard keeps working.
  • My site has keyboard shortcuts. While the phone has focus, it claims the keys, so pressing A types an "a" instead of opening my About page.
  • Escape. The case study overlay closes on Escape. Inside the phone, Escape closes the sheet first and stops there.

That's the whole list. None of it shows up in a screenshot. All of it shows up the moment someone taps.