Nobody Noticed, Because Everyone Was on a Desktop

Two claims were in my documents for months and both were false on a phone. A narrow window is not a phone: responsive layout is a claim about composition, never about input.

Date

/

Category

UI/UX

UI/UX

/

Writer

Keon Hwui Kim

Keon Hwui Kim

Two claims had been in my documents for months. Both stated confidently. Both repeated in several reports.

"It works offline."
"It accepts anything. Universal intake."

Both were false on a phone. Neither had been noticed, because every round of development had been driven from a desktop.

What the measurement showed

I finally drove the product on an emulated iPhone rather than a narrowed desktop window. Two findings, both fatal, both invisible from a laptop.

Nothing could get in

The only paths into the product were drag and paste. Touch has neither.

A finger never fires a dragstart event. HTML5 drag and drop simply does not exist on touch, as a matter of specification rather than implementation gap. And there is no paste gesture available to a web page in the way the product needed.

So on a phone, the entire capture surface of a product whose thesis is drop anything here was: typing text.

That is it. Not degraded. Not awkward. Absent.

And it did not open offline

There was no service worker, so the app shell was never cached, so with the network off the product did not load at all.

The data was local, which was the part I had been checking, and completely unreachable, which was the part I had not.

Why "responsive" hid it

This is the part worth dwelling on, because the failure is a design failure rather than an engineering one.

The layout was responsive. At phone width it reflowed correctly: no horizontal overflow, readable type, sensible spacing, tappable targets. It looked like a working mobile product in every screenshot anyone took.

Responsive layout is a claim about composition. It says the content arranges itself for the viewport. It makes no claim whatsoever about input, about whether the gestures your product depends on exist on that device.

And we test the first constantly and the second almost never, because the first is visible in a screenshot and the second is not.

A narrow window is not a phone. It is a desktop that has been made narrow, and it still has a pointer, a keyboard, a clipboard, and drag events.

Every mobile check I had run was in a narrowed browser. Every one passed. All of them were testing composition, and the failure was in input.

The assumptions I was swimming in

Writing them down afterwards was more uncomfortable than fixing the bug.

Assumption

True on desktop

True on touch

Load-bearing?

A pointer exists and can hover

Yes

No

Yes. Half the affordances

A keyboard is always present

Yes

No

Yes. Every shortcut

The clipboard is reachable

Yes

Not by gesture

Yes. It was half of intake

dragstart fires

Yes

Never

Yes. It was the other half

The network is up

Usually

Often not

Yes. Nothing was cached

The window is wide

Yes

No

No. This one I had handled

Table 1. Five of six were load-bearing and unwritten. The one I had handled is the only one visible in a screenshot.

None of these were written down anywhere, because nobody writes down what has always been true. They were not decisions. They were the water.

The fixes, and what they cost

For intake: a + in the leave bar that opens the platform's own file picker and feeds the existing capture path. No new pipeline. The same code that handles a dropped file handles a picked one.

The fix is small precisely because the architecture underneath was sound. The product had a door and simply no handle on that side. That is worth noting as a positive result about the earlier work: a badly structured product would have needed a parallel mobile ingest path, and would probably still have one today.

For offline: a service worker that caches the shell network-first, and never caches the API paths. Network-first rather than cache-first, so a stale shell is never preferred over a live one.

Then a new gate, 21 assertions driven against the built site rather than a dev server, so that neither claim can quietly become false again.

The touch work underneath

Making things movable by finger was more interesting than expected, and the core problem is genuinely hard.

The ambiguity: a finger pressing a card and then moving might be starting a drag, or might be scrolling the page. You cannot know at touch-start, and you must not guess wrong, because guessing wrong either makes the page unscrollable or makes the cards immovable. Both are catastrophic and each is the natural failure of guessing the other way.

The resolution:

press
  ├─ stays still past the dwell threshold  →  LIFT
  └─ moves before the threshold            →  SCROLL

// Dwell disambiguates. Nothing else can.
// And once lifted, the same function computes
// the drop target for both mouse and finger.
press
  ├─ stays still past the dwell threshold  →  LIFT
  └─ moves before the threshold            →  SCROLL

// Dwell disambiguates. Nothing else can.
// And once lifted, the same function computes
// the drop target for both mouse and finger.
press
  ├─ stays still past the dwell threshold  →  LIFT
  └─ moves before the threshold            →  SCROLL

// Dwell disambiguates. Nothing else can.
// And once lifted, the same function computes
// the drop target for both mouse and finger.

This is what every mature touch interface converged on, and rediscovering it from first principles made me appreciate why rather than merely copying it.

The second line matters as much as the first. Both hands share one function for computing where a drop would land. Not two implementations kept in agreement. Behaviour divergence between mouse and touch is almost always two code paths drifting apart over months, and the only reliable fix is to not have two.

The platform limits I could not design around

Two, stated because pretending otherwise would be the easy move.

iOS share-sheet capture is impossible for a web application. There is no web share target on iOS. Only a native share extension can register there. This is not a gap to work around, it is a capability that does not exist, and saying so is more useful than shipping something that approximates it badly.

Android file sharing needs a POST multipart target intercepted by a service worker. That is cheap now that a worker exists, but it changes the GET path that currently works, so it is classified as unknown and deliberately not built. A working simple thing is worth more than a possibly-working better thing until the better thing is actually required.

The operating change

The lesson is not "test on mobile." Everyone says that and everyone believes they already do.

The precise version:

The device you develop on is a set of assumptions you cannot see.

Not because you are careless. Because those assumptions are the medium you are working in. A pointer that hovers. A keyboard always present. A clipboard reachable by shortcut. Drag events that fire. A network that is up.

The only method I have found that surfaces them is to run the real product, on the real class of device, doing the real task, and to treat any claim that has never been checked there as unverified rather than true.

Two claims, months old, in several reports. Both false. The cost of finding out was one afternoon on an emulated phone, and the cost of not finding out was every conclusion I drew in between.

Related reading

More from the Moc Mallen Studio journal