Handing a Swipe From the Finger Back to Navigation
A Gesture Has Two Owners
Tango Explorer displays one live stream while keeping its neighbors ready. The video manager maintains three reusable units:
units[0] = previous
units[1] = current
units[2] = next
After navigation, the array rotates and the unit that left the visible window is recycled for another streamer. This makes normal next/previous navigation cheap because the destination video already exists.
It also makes a peek gesture possible. While I drag vertically, the current stream follows my finger and reveals the neighboring stream behind it. Releasing past a threshold commits the navigation; releasing early returns to the current stream.
The transforms were straightforward. The difficult part was deciding when the gesture stopped owning the screen and the normal navigation system resumed ownership.
While the Finger Owns the Screen
On every touchmove, the gesture controller reports the vertical distance
dy. It does not know about videos or carousel rotation. The video manager
chooses the relevant neighbor and positions both units:
const active = units[1];
const peek = dy < 0 ? units[2] : units[0];
active.element.style.transform =
`translateY(${dy}px)`;
peek.element.style.transform = dy < 0
? `translateY(${dy + innerHeight}px)`
: `translateY(${dy - innerHeight}px)`;
Transitions are disabled during this phase. The finger is the animation clock; adding easing would make the interface lag behind it.
The unused neighbor is hidden explicitly. Without that step, a fast drag could expose both surrounding units at opposite edges.
Commit or Cancel
Release commits only when two conditions are true:
const commit =
Math.abs(dy) > innerHeight * NAV_COMMIT_THRESHOLD
&& peekUnit.hasContent;
Distance alone is insufficient. At the end of the stream list, the neighboring unit may be empty. The interface can still resist the finger naturally, but it must not navigate into an absent stream.
For a commit, CSS transitions move the active unit out of the viewport and the
peek unit to translateY(0). For a cancellation, both return to their starting
positions.
During either animation, the gesture controller rejects new gestures. A
transitionend callback marks the handoff point.
The Flash at the Handoff
Tango Explorer already had event-driven navigation:
UI.NEXT
→ application state advances
→ video manager rotates units
→ recycled unit receives new content
→ visibility is normalized
Emitting UI.NEXT before the gesture animation finished caused the carousel to
rotate while the finger-controlled transforms were still active. Cleaning up
too late left transforms attached to units whose roles had changed.
The stable ordering is:
- Finish the visual transition.
- Clear every temporary transform and transition.
- Restore the normal three-unit visibility convention.
- Emit
UI.NEXTorUI.PREVIOUS. - Let the existing navigation rotate and repopulate the units.
const onEnd = () => {
clearPeekStyles();
emitter.emit(dy < 0 ? Events.UI.NEXT : Events.UI.PREVIOUS);
onDone();
};
At first, clearing styles before emitting navigation sounds as if it should
briefly reveal the old current stream. In practice the cleanup and event
handling occur synchronously in the same task. The destination unit is already
at the center when the animation ends, and normal navigation immediately
adopts it as units[1].
The key is that cleanup does not invent a second navigation model. It returns the DOM to the exact steady-state convention that the existing model expects.
Cancellation Uses the Same Exit
Cancellation animates the active unit to the center and the neighbor back outside the viewport. It then calls the same cleanup routine without emitting a navigation event.
touchcancel also calls cleanup directly. This matters on iPhone because a
gesture can be interrupted by browser UI, an incoming system gesture, or a
change in touch tracking. Temporary transforms must never become permanent
application state.
function clearPeekStyles() {
for (let i = 0; i < units.length; i++) {
const element = units[i].element;
element.style.transform = "";
element.style.transition = "";
if (i !== 1) element.hidden = true;
}
}
The General Lesson
Continuous interaction and application navigation operate at different timescales.
While the finger is down, direct DOM transforms are the simplest source of truth. At release, the existing event-driven application should regain control. The boundary between them needs an explicit protocol:
acquire temporary visual control
→ track the gesture
→ animate commit or cancellation
→ remove every temporary mutation
→ optionally emit one discrete navigation
The animation was never the difficult part. The difficult part was handing a screen that had been continuously controlled by a finger back to a system that understands only discrete “next” and “previous” events.
Once that boundary was explicit, the peek gesture stopped being a second navigation implementation. It became a temporary visual preview that ends by asking the original navigation system to perform exactly one operation.