Source Code

<section class="mtg-wrap">
  <span class="mtg-tag">event.touches · live debug view</span>
  <h1>Touch point visualizer</h1>
  <p id="mtgStatus">On a touchscreen, place multiple fingers on the pad below \u2014 each gets a live numbered marker. On desktop, drag the mouse to simulate a single point.</p>

  <div class="mtg-pad" id="mtgPad">
    <p class="mtg-pad-hint" id="mtgPadHint">Touch or click-drag here</p>
  </div>

  <div class="mtg-legend">
    <span class="mtg-count" id="mtgCount">0 active points</span>
    <span class="mtg-mode" id="mtgMode">Waiting for input\u2026</span>
  </div>
</section>

Multi-Touch Gesture Visualizer — Free event.touches Debug Snippet

Multi-Touch Gesture Visualizer · Mobile · Plain HTML, CSS & JS · Live preview

What's included

Features

Real event.touches tracking
Every active finger renders its own live marker.
Stable per-finger identifiers
Touch.identifier keeps markers attached correctly.
touches vs changedTouches
Correct list used for start/move vs end events.
Desktop mouse fallback
A single-point simulation for non-touch devices.
Shared rendering path
Touch and mouse drive the same marker functions.
Live point count
A legend reports how many fingers are active.
Color-coded markers
Each new point gets a distinct color.
No dependency
Pure DOM touch events, no gesture library.

About this UI Snippet

Multi-Touch Gesture Visualizer — Every Real Touch Point, Live

Screenshot of the Multi-Touch Gesture Visualizer snippet rendered live

This is a debugging and teaching tool: place multiple fingers on the pad and each one gets its own numbered marker that tracks that exact finger in real time, using the browser's real event.touches list rather than a simulated animation. It is the same class of raw touch data that drives the pinch math in pinch & scroll zoom image viewer, made visible on its own.

event.touches vs. event.changedTouches

Touch events expose two different lists that are easy to conflate. event.touches is *every* finger currently on the surface, used here in touchmove to reposition every active marker each frame. event.changedTouches is only the fingers that changed *in this specific event* — new ones in touchstart, lifted ones in touchend — which is exactly what's needed to create a marker for a finger that just landed or remove one that just lifted, without touching the markers for fingers that didn't change.

Stable identifiers keep markers attached to the right finger

Each Touch object carries an identifier that stays constant for that specific finger across every event until it lifts. Markers are keyed by that identifier in a markers object, so when you move three fingers at once, each marker tracks the one finger it was created for — swapping which finger moves fastest never causes two markers to jump or swap positions, because the mapping is by identifier, not by array order (which touch order is not guaranteed to preserve).

Why desktop needs a fallback path

A typical desktop has no touchscreen, so there is nothing real to visualize with event.touches — showing a blank pad would make the demo look broken rather than platform-limited. A parallel mousedown/mousemove/mouseup path drives the exact same positionMarker/removeMarker functions with a single synthetic identifier ('mouse'), so desktop visitors still see the marker mechanics working, clearly labeled as a single-point simulation rather than real multi-touch.

One rendering path, two input sources

Both the touch handlers and the mouse handlers funnel through the same markerFor/positionMarker/removeMarker/updateLegend functions — the only difference between the real and simulated paths is which identifier is used and how many points can exist at once. This keeps the marker rendering logic honest: whatever you see on a touchscreen is driven by the identical code path, just fed by real hardware instead of a mouse.

Customizing it

Add per-finger trail history to visualize gesture paths, color-code markers by gesture type, or feed the same touches list into a custom pinch/rotate detector modeled on pinch & scroll zoom image viewer. Pair it with swipe cards as a debugging aid while building your own gesture.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to puzzle out the touches versus changedTouches distinction on your own. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why touchmove reads from event.touches (every currently active finger) while touchstart and touchend read from event.changedTouches (only the fingers that changed in that specific event), and how each Touch object's stable identifier is what lets a marker stay attached to one specific finger across many events instead of jumping between whichever touch happens to be reported first. The same assistant can help you optimize it — for instance asking whether repositioning every marker's left/top via direct style writes on every touchmove could be batched inside a single requestAnimationFrame for smoother rendering with many simultaneous fingers. It's also useful for extending the visualizer: ask it to draw a fading trail behind each marker showing its recent path, compute and display the live distance and angle between exactly two touch points (the same math a pinch-to-zoom or rotate gesture would use), or add a pressure/force readout for devices that report Touch.force. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "multi-touch gesture visualizer" in plain HTML, CSS, and JavaScript using the real Touch Events API — no gesture library, no simulated/fake touch data.

Requirements:
- A bounded pad element that listens for touchstart, touchmove, touchend, and touchcancel, with touch-action: none so gestures on it don't scroll or zoom the page.
- On touchstart and touchend, iterate event.changedTouches (only the touches that changed in that specific event) to create a new marker for each newly-placed finger or remove the marker for each newly-lifted finger. On touchmove, iterate the full event.touches list (every currently active finger) to reposition every active marker in real time.
- Track each marker using the corresponding Touch object's stable identifier property as a key (e.g. in a plain object or Map), not by array index or order, so that with multiple fingers down simultaneously each marker continues to track the exact same physical finger throughout the gesture even as other fingers join or leave.
- Position each marker using getBoundingClientRect() on the pad combined with the touch's clientX/clientY, rendering a distinctly colored, numbered circle exactly at that live coordinate.
- Since a typical desktop computer has no real multi-touch hardware, add a mousedown/mousemove/mouseup fallback path that drives the identical marker-creation/position/removal functions using a single fixed identifier, so desktop visitors see a working single-point simulation rather than a blank, apparently broken pad — and label in the UI whether the current input is real touch or the mouse simulation.
- Display a live count of currently active points and ensure that lifting all fingers (or releasing the mouse) fully clears all markers and resets the display back to its empty state.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSAn empty touch pad renders with a hint in the center.
  2. 2
    On a touchscreen, place a fingerA numbered, colored marker appears at that exact point.
  3. 3
    Add more fingersEach gets its own marker that tracks it independently.
  4. 4
    Move your fingersMarkers follow live via touchmove and event.touches.
  5. 5
    Lift a fingerIts marker disappears; the others keep tracking correctly.
  6. 6
    On desktop, click-dragA single simulated marker follows the mouse instead.

Real-world uses

Common Use Cases

Gesture debugging
See exactly what event.touches reports while developing.
QA and device testing
Verify multi-touch hardware reports points correctly.
Teaching touch events
A live reference for touches vs changedTouches.
Drawing and canvas apps
Prototype multi-finger input before wiring real drawing.
Game input prototyping
Visualize multi-touch controls during development.
Pairing with gesture snippets
Debug alongside pinch & scroll zoom.
Related: Mobile Search Screen
See the Mobile Search Screen for a related mobile pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

event.touches is the complete list of every finger currently touching the surface at the moment of the event, used here to reposition all active markers on every touchmove. event.changedTouches is only the subset of fingers that changed specifically in this event — newly placed fingers in touchstart, or newly lifted ones in touchend — which is exactly the right list for creating or removing individual markers without disturbing the ones that did not change.

Every Touch object has an identifier property that remains constant for that specific finger from the moment it touches down until it lifts, even while other fingers join or leave. Markers are stored in an object keyed by that identifier, so repositioning or removing a marker always targets the exact finger it was created for, regardless of the order fingers appear in the touches array on any given event.

A typical desktop computer has no touchscreen, so event.touches would simply never fire there, leaving the demo pad blank and looking broken rather than platform-limited. A parallel mousedown/mousemove/mouseup path drives the identical marker-rendering functions with a single synthetic identifier, clearly labeled in the UI as a one-point mouse simulation rather than real multi-touch.

No — both paths call the same markerFor, positionMarker, removeMarker, and updateLegend functions. The only difference is which identifier is used (a real touch's stable identifier versus the fixed string 'mouse') and how many simultaneous points can exist (many on real touch, exactly one via mouse), so the visualization logic itself is identical and trustworthy either way.

Keep the markers map in a ref rather than state, since it is mutated on every touchmove and doesn't need to trigger a re-render itself — instead, imperatively create/update/remove DOM nodes for markers the way the vanilla version does, or maintain a small array in state updated only on touchstart/touchend (not touchmove) if you want React to own the marker elements. Attach listeners to the pad's ref in a mount effect with cleanup.