webdevpuneet.com

Long-Press Preview (iOS Peek) — Free Hold-to-Preview Snippet

Long-Press Preview (iOS-Style Peek) · Modals · Plain HTML, CSS & JS · Live preview

CategoryModals

Copy & paste

Source Code

<div class="ltp-wrap">
  <p class="ltp-hint">Press and hold a card (mouse or touch) to peek at its full content</p>
  <div class="ltp-grid">
    <article class="ltp-card" data-preview="A full case study covering discovery, design iterations, and the final shipped redesign \u2014 including the metrics that moved after launch.">
      <span class="ltp-tag">Case study</span>
      <h3>Redesigning checkout</h3>
      <p>Conversion research and the flow that replaced it\u2026</p>
    </article>
    <article class="ltp-card" data-preview="A deep dive into the token pipeline: naming conventions, the build step that generates platform-specific output, and how design and engineering stay in sync.">
      <span class="ltp-tag">Engineering</span>
      <h3>Our design token pipeline</h3>
      <p>How tokens flow from Figma to production\u2026</p>
    </article>
    <article class="ltp-card" data-preview="Interviews with three teams who moved off spreadsheets, what broke in the first month, and the checklist we now hand every new customer.">
      <span class="ltp-tag">Customers</span>
      <h3>Migrating off spreadsheets</h3>
      <p>Three teams share what actually happened\u2026</p>
    </article>
  </div>
</div>
<div class="ltp-peek" id="ltpPeek">
  <p class="ltp-peek-label" id="ltpPeekLabel"></p>
  <p class="ltp-peek-text" id="ltpPeekText"></p>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Real hold-duration timing
A cancellable setTimeout, not a CSS animation delay.
Movement-tolerant detection
Math.hypot distinguishes a hold from a drag or scroll.
Press-point positioning
The peek appears near where you actually pressed.
Edge-aware flip
Flips below the press point when there's no room above.
Instant release dismissal
No separate tap-away step needed.
Native menu suppressed
contextmenu is prevented so iOS doesn't show its own callout.
Works with mouse and touch
Pointer Events unify both input types.
No dependency
Pure HTML/CSS/JS, no tooltip library.

About this UI Snippet

Long-Press Preview — The Touch Equivalent of a Hover Tooltip

Screenshot of the Long-Press Preview (iOS-Style Peek) snippet rendered live

Touchscreens have no hover state, so a tooltip that only appears on :hover is invisible on mobile. This snippet is iOS's "Peek" pattern rebuilt in plain JavaScript: pressing and holding a card past a real time threshold, without dragging, pops up a preview of its expanded content near where you pressed — release and it disappears. It uses genuine pointerdown timing and movement tracking, not a CSS :active state or a fixed-delay animation.

A timer that can be cancelled

pointerdown starts a setTimeout for PRESS_MS (500ms) and immediately marks the card as .pressing for visual feedback. If the pointer lifts (pointerup) or leaves before that timer fires, clearPress() cancels it — so a normal tap, which lifts in well under 500ms, never triggers a peek at all. Only a press held continuously past the threshold ever calls showPeek().

Movement tolerance distinguishes a hold from a scroll

A long press has to reject drags and scroll gestures too, not just quick taps. onPressMove computes the distance from the press's starting point with Math.hypot, and any movement past MOVE_TOLERANCE (10px) cancels the pending timer and hides an already-shown peek — so starting to scroll the page, or dragging past the card, correctly falls through to normal scrolling instead of triggering (or getting stuck showing) a preview.

Positioned near the press point, with edge awareness

positionPeek clamps the peek horizontally so it never overflows the viewport's left or right edge, and checks whether there's enough room above the press point (top < 140) to flip the peek below it instead — the same edge-awareness a well-built context menu needs, applied to a preview bubble instead of a menu.

Release dismisses immediately

pointerup and pointercancel both call onPressEnd, which clears any pending timer and hides the peek if it's showing — there's no separate "tap elsewhere to dismiss" step, since a peek is meant to be a momentary preview tied entirely to the press, exactly like iOS's own Peek gesture.

Customizing it

Tune PRESS_MS and MOVE_TOLERANCE, add a "Pop" step where a second press-through commits to full navigation (iOS's Peek-and-Pop), or swap the plain text preview for rendered HTML. Pair it with long-press confirm with radial fill for a different real hold-timing mechanic, or image magnifier for a hover-based preview on desktop.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA grid of cards renders with a press-and-hold hint.
  2. 2
    Press and hold a cardNothing happens for the first 500ms besides a subtle press state.
  3. 3
    Keep holding past 500msA preview peek appears near where you pressed.
  4. 4
    ReleaseThe peek disappears immediately.
  5. 5
    Drag or scroll insteadMovement past a small tolerance cancels the peek entirely.
  6. 6
    Tune the feelChange PRESS_MS and MOVE_TOLERANCE.

Real-world uses

Common Use Cases

Article and blog cards
Let touch users peek at a full summary before committing to a tap, using a cancellable setTimeout for a true hold threshold.
Product cards
Preview details without leaving a grid, with Math.hypot distinguishing a deliberate hold from a drag or scroll gesture.
Contact and profile lists
Hold a contact to preview information near where the finger pressed, flipping below the press point when there is no room above.
Link previews
Provide the touch equivalent of hovering a link on desktop, where :hover tooltips are simply invisible on phones.
Hover tooltip comparison and timing learning
Use as the touch-first counterpart to hover tooltips, and as a reference for pointerdown press timing and movement tolerance.
Related: Resizable, Draggable Floating Modal Window
See the Resizable, Draggable Floating Modal Window for a related modals pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

pointerdown starts a setTimeout for a fixed duration (500ms by default). A normal tap releases well before that timer fires, and pointerup immediately cancels the pending timer via clearTimeout, so the preview never appears. Only a press held continuously past the full duration lets the timer run to completion and call showPeek() — a quick tap and a long press are structurally different outcomes of the same timer, not two separate detection paths.

Every pointermove event while a press is active computes the straight-line distance from the press's starting coordinates using Math.hypot. If that distance exceeds MOVE_TOLERANCE (10px), the pending timer is cancelled and any already-shown peek is hidden immediately — so starting to scroll the page, or dragging away from the card, correctly cancels the gesture instead of accidentally showing (or leaving stuck) a preview.

positionPeek() centers the peek horizontally on the press coordinate, then clamps that position so the peek's fixed width never overflows the viewport's left or right edge. It also checks whether there's enough vertical room above the press point; if not, it flips the peek to appear below the press point instead, the same edge-awareness pattern used in a well-built context menu.

On many touch browsers, holding a finger in place for roughly half a second also triggers the operating system's own long-press callout menu (e.g. "Copy", "Share") via the contextmenu event, which would visually collide with this snippet's own custom peek. Calling e.preventDefault() on contextmenu suppresses that native menu so only the custom preview appears.

Keep the timer id and the press-start coordinates in refs (not state, since they update on every pointermove without needing a re-render), and keep only the shown boolean and preview content in state so React re-renders the peek's visibility and text. Attach the pointer handlers per card via refs or event delegation, and clear the timer in a cleanup function on unmount.