npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@excom/gesture-handler

v0.2.4

Published

<gesture-handler> custom element

Readme

gesture-handler

Let users drag, swipe, pinch and flick your UI — sheets, carousels, cards and images follow the finger, in CSS, with no per-frame script.

<gesture-handler gesture-types="pan-y swipe" pointer-types="touch pen mouse" from-ref=":scope > content-drawer > header"
  handoff-ref=":scope > content-drawer" progress-axis="up" range-ref=":scope > content-drawer" snap-points="0 1">
  <quark-sheet>
    :scope {
      /* dragging an open sheet starts from fully open */
      &:has(> content-drawer[is-open]) {
        progress-offset: 1;
      }
      &:not(:has(> content-drawer[is-open])) {
        progress-offset: 0;
      }
      @on gesture-handler-start {
        content-drawer { is-scrubbing: ""; }
      }
      @on gesture-handler-end {
        content-drawer { is-open: event.detail.snap == 1; is-scrubbing: none; }
      }
    }
  </quark-sheet>
  <button type="button" command="--open" commandfor="demo-gesture-sheet">Open sheet</button>
  <content-drawer id="demo-gesture-sheet" class="absolute">
    <header><h2>Drag me</h2></header>
    <p>Two ways to dismiss it. Drag the header down and let go, or flick it —
      that is the <code>from-ref</code> grab handle.</p>
    <p>Or pull the text itself down: <code>handoff-ref</code> lets the sheet's
      own content scroll first, and only once it is back at the top does the
      next pull down become the same drag. Pulling up, or pulling down while
      the text is scrolled, keeps scrolling as usual.</p>
    <p>Scroll on. The sheet only takes the drag over at the very top, which is
      how a native sheet behaves.</p>
    <p>Keep going.</p>
    <p>Nearly there.</p>
    <p>Now scroll back up and pull once more — the sheet follows your finger
      and snaps to <code>0</code> or <code>1</code> on release.</p>
  </content-drawer>
  <style>
    #demo-gesture-handler-sheet > :first-child {
      /* short enough that the text scrolls: the handoff needs a scroller */
      content-drawer { max-block-size: 11rem; }
      /* the drag handle owns its touches; the content keeps scrolling */
      content-drawer > header {
        position: sticky;
        top: 0;
        touch-action: none;
        cursor: grab;
        background-color: inherit;
      }
    }
  </style>
</gesture-handler>

Features

  • Swipe / pan / pinch / rotate / tap / long-press Each recognized gesture is a tag-prefixed event
  • Follow the finger in CSS Every frame lands in --gesture-* custom properties: scrubbable bottom sheets, swipe-to-dismiss, pinch-to-zoom, pull-to-refresh, parallax
  • Snap & fling snap-points with velocity projection, a CSS transition on release (--gesture-snap-duration / --gesture-snap-ease), gesture-handler-snap when it lands
  • Drivable Bounds, progress-offset and is-disabled are attributes a Quark rule sets from the driven element's state
  • Scoped starts from-ref for drag handles, from-edge for edge swipes
  • Scroll handoff handoff-ref lets a sheet's own scrolling content take the drag over when it runs out of scroll — a native-feeling pull-to-close
  • Native scrolling kept touch-action follows gesture-types, so the page still scrolls where you don't pan

Installation

@excom/gesture-handler v0.2.4

pnpm add @excom/gesture-handler
npm install @excom/gesture-handler
yarn add @excom/gesture-handler

Import

import "@excom/gesture-handler";

Usage

Wrap the surface the user touches. Pick the gestures with gesture-types; read the finger from --gesture-* in CSS (custom properties inherit, so any descendant can var() them); commit State on gesture-handler-end or -snap from a Quark @on block.

<gesture-handler gesture-types="pan-y swipe" progress-axis="up" range-ref=":scope > content-drawer" snap-points="0 1">
  <quark-sheet>
    :scope {
      @on gesture-handler-start { content-drawer { is-scrubbing: ""; } }
      @on gesture-handler-end { content-drawer { is-open: event.detail.snap == 1; is-scrubbing: none; } }
    }
  </quark-sheet>
  <content-drawer>…</content-drawer>
</gesture-handler>

--gesture-progress is the travel along progress-axis as a fraction of the range (range-ref measures the driven element; range-px is a literal), clamped to progress-min..progress-max with optional overshoot-resistance. Written alongside it every frame: --gesture-dx / -dy, --gesture-x / -y, --gesture-scale, --gesture-rotate, --gesture-vx / -vy, --gesture-pointers; once per gesture: --gesture-range-px, --gesture-width / -height. Everything else is derived from those in the element's own CSS — --gesture-distance, --gesture-angle, --gesture-progress-px, --gesture-x-ratio / -y-ratio — so var() them the same way.

Values persist after release until the next gesture starts. On release the default action writes --gesture-progress straight to detail.snap and the element's own transition settles it there over --gesture-snap-duration (200ms) with --gesture-snap-ease (ease-out), firing gesture-handler-snap when it lands; the transition is off while is-active, so the finger itself is never eased, and a new gesture that interrupts the settle simply cancels it (no -snap).

Driving NucleusKit elements

Elements that can be scrubbed expose a --<tag>-…-progress input and an is-scrubbing attribute; both default to the wrapping gesture-handler's --gesture-progress, so no mapping is needed:

| Element | Set up | While is-scrubbing | | --- | --- | --- | | content-drawer | progress-axis towards its open side, range-ref the drawer, handoff-ref the drawer too (it scrolls its own content) | Position follows --content-drawer-open-progress (0 closed, 1 open), no transition | | content-carousel | slide-animation="track", progress-min="-1" progress-max="1" snap-points="-1 0 1" | The track follows --content-carousel-progress in slide widths |

The handoff is one Quark commit: the block that writes the final state (is-open, the active slide) also removes is-scrubbing, so the element switches from finger to State in the same paint. Commit on -end when a CSS transition should finish the motion (the drawer), on -snap when the element must be exactly at the snap point first (the carousel).

Anything else follows the same recipe: read --gesture-* in your own CSS, gate the mapping on a fact your sheet writes on start and clears on end.

Give a drag handle touch-action: none when using from-ref, so the browser does not scroll it away; without from-ref the element sets touch-action itself from gesture-types. For pan-x / pan-y, a touch whose first 3 px run along the handler's axis holds the page still for the rest of that touch (a non-passive touchmove listener does this; touch-action is unchanged), while a touch that starts across the axis scrolls the page as before. Add mouse to pointer-types for desktop dragging.

Scroll handoff

handoff-ref names the scroll container(s) inside the surface whose overscroll starts a gesture (a :scope-relative selector; a comma list matches several). A pointer that goes down in one of them scrolls natively as usual. Only when its first move runs along progress-axis, nothing between the pointer and the element can still scroll that way (the named containers and any scroll container nested in or around them, so a scrolled editor inside a sheet scrolls back first), and progress-offset still has room to travel in that direction does the element cancel the native scroll for the rest of the touch and take the drag over — the same gesture, the same --gesture-* values and the same -start / -end / -snap events as a drag from a handle:

<gesture-handler gesture-types="pan-y swipe" progress-axis="up" snap-points="0 1"
  from-ref=":scope > content-drawer > header" handoff-ref=":scope > content-drawer"
  range-ref=":scope > content-drawer">

An open sheet (progress-offset: 1) closes either from its header or by pulling its text down once the text is back at the top; pulling up, or pulling down mid-scroll, keeps scrolling. handoff-ref is additive — from-ref and from-edge starts are unchanged, and a from-ref handle inside a handoff container still starts on pointerdown. The element keeps touch-action out of the way while handoff-ref is set (the containers must be able to scroll), so give handles their own touch-action: none.

Mouse drags (pointer-types="… mouse") take the same route with no native scroll to cancel: the first pointermove inside the container starts the gesture when nothing below the pointer can still scroll that way.

API Reference

Attributes

| Name | Surface | Type | Default | Values | Description | | --- | --- | --- | --- | --- | --- | | gesture-types | option | tokenlist | "pan" | "pan" | "pan-x" | "pan-y" | "pinch" | "rotate" | "swipe" | "tap" | "double-tap" | "long-press" | Gestures to recognize. pan-x / pan-y one axis (a touch starting along it holds the page still, one across it scrolls); pan both; pinch / rotate need two fingers; swipe velocity on release; tap / double-tap / long-press fire events. | | from-ref | option | string | | <CSS Selector> | Only start when pointerdown is inside this descendant (:scope-relative selector) — a drag handle, the sheet itself. Combined with from-edge, either qualifies. | | from-edge | option | tokenlist | | "left" | "right" | "top" | "bottom" | Only start within edge-px of these edges — edge swipes (back nav, pulling a closed sheet up). | | edge-px | option | number | 40 | | Width of from-edge start zone (px). | | handoff-ref | option | string | | <CSS Selector> | Scroll containers that hand overscroll to the gesture (:scope-relative selector, comma list matches several) — e.g. a sheet closed by pulling its own content down. Pointerdown inside one scrolls natively; the gesture takes over only when the first move runs along progress-axis, every scroller between pointer and this element is at its limit that way, and progress-offset has room. Additive to from-ref / from-edge. | | pointer-types | option | tokenlist | "touch pen" | "touch" | "pen" | "mouse" | Pointer types that can start a gesture. Add mouse for desktop drag. | | max-pointers | option | number | | | Extra pointers beyond this many are ignored. Unset = 2 when pinch / rotate listed, else 1. | | threshold-px | option | number | 8 | | Movement (px) before a pan is recognized. Taps / native scroll stay untouched below it. | | lock-axis | option | boolean | | | A free pan locks to its dominant axis once recognized (gesture-type becomes pan-x / pan-y). | | arm-after | option | string | | "long-press" | Pans only arm after this gesture — long-press for hold-then-drag. | | long-press-ms | option | number | 500 | | Hold time (ms) for long-press; also tap time limit. | | double-tap-ms | option | number | 300 | | Max gap (ms) between taps for double-tap. | | swipe-min-velocity | option | number | 0.5 | | Release velocity (px/ms) that counts as a swipe. | | swipe-directions | option | tokenlist | | "left" | "right" | "up" | "down" | Swipe directions to report. Unset = all four. | | progress-axis | option | string | | "up" | "down" | "left" | "right" | Direction --gesture-progress grows. Unset = down for a pan-y-only element, else right. | | range-ref | option | string | | <CSS Selector> | Element whose size along progress-axis is the range of progress 0..1 (:scope-relative, read once per gesture) — sheet being dragged, slide being swiped. | | range-px | option | number | | | Literal range (px) instead of range-ref. | | progress-min | option | number | 0 | | Lower bound of progress. | | progress-max | option | number | 1 | | Upper bound of progress. | | progress-offset | option | number | 0 | | Progress the gesture starts from. Set from a rule that reads the driven element's state (1 while a sheet is open) so dragging it closed starts full. | | overshoot-resistance | option | number | 0 | | Rubber-band past progress-min / progress-max: 0 clamps, 0.3 overshoots at a third of travel. | | snap-points | option | tokenlist | | <number>… | Progress values to settle on after release (0 0.5 1). Target picked from position, fling velocity and swipe direction, reported as detail.snap on -end, settled on by the default action (a CSS transition, --gesture-snap-duration / --gesture-snap-ease). | | should-emit-move | option | boolean | | | Fire gesture-handler-move every frame. Off by default; --gesture-* is enough for CSS. | | is-disabled | hybrid | boolean | | | Ignore new pointers; a gesture in progress is cancelled. State-driven veto. | | is-active | state | boolean | | | A pointer is down on the surface. Set from first pointer until release, so it also covers taps and pre-threshold phase. | | gesture-type | state | string | | "pan" | "pan-x" | "pan-y" | "pinch" | "rotate" | Recognized gesture in progress, unset before recognition and after release. | | pointer-count | state | number | | | Pointers currently down. | | gesture-direction | state | string | | "left" | "right" | "up" | "down" | Dominant travel direction of gesture in progress. | | last-gesture | state | string | | "pan" | "pan-x" | "pan-y" | "pinch" | "rotate" | "swipe-left" | "swipe-right" | "swipe-up" | "swipe-down" | "tap" | "double-tap" | "long-press" | What last gesture turned out to be — style a "just swiped" state from it. |

Provision

| Name | Type | Description | | --- | --- | --- | | provision | GestureHandlerProvision ({ x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: \| "pan" \| "pan-x" \| "pan-y" \| "pinch" \| "rotate" \| "swipe" \| "tap" \| "double-tap" \| "long-press" \| null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number \| null; swipe: "left" \| "right" \| "up" \| "down" \| null; }) | Last -start / -end / -cancel snapshot (type, travel, velocity, progress, snap, swipe, …). Not an attribute; per-frame values live in --gesture-*. |

Fires

| Name | Type | Description | | --- | --- | --- | | gesture-handler-start | GestureHandlerStartEvent (CustomEvent & { type: "gesture-handler-start"; detail: { x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: \| "pan" \| "pan-x" \| "pan-y" \| "pinch" \| "rotate" \| "swipe" \| "tap" \| "double-tap" \| "long-press" \| null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number \| null; swipe: "left" \| "right" \| "up" \| "down" \| null; }; bubbles: true; cancelable: true; composed: true }) | Gesture recognized: pan passed threshold-px (after arm-after if set), or a second finger for pinch / rotate. After gesture-type + provision set. detail = provision. Not cancelable — gate with is-disabled. | | gesture-handler-move | GestureHandlerMoveEvent (CustomEvent & { type: "gesture-handler-move"; detail: { x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: \| "pan" \| "pan-x" \| "pan-y" \| "pinch" \| "rotate" \| "swipe" \| "tap" \| "double-tap" \| "long-press" \| null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number \| null; swipe: "left" \| "right" \| "up" \| "down" \| null; }; bubbles: true; cancelable: true; composed: true }) | Once per frame while a recognized gesture moves, only with should-emit-move. detail = provision. --gesture-* always updates, event or not. | | gesture-handler-end | GestureHandlerEndEvent (CustomEvent & { type: "gesture-handler-end"; detail: { x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: \| "pan" \| "pan-x" \| "pan-y" \| "pinch" \| "rotate" \| "swipe" \| "tap" \| "double-tap" \| "long-press" \| null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number \| null; swipe: "left" \| "right" \| "up" \| "down" \| null; }; bubbles: true; cancelable: true; composed: true }) | Last pointer up after a recognized gesture. After is-active unset, last-gesture + provision set, and after gesture-handler-swipe if one was recognized. detail.snap = snap-points target from position / velocity / swipe (null without snap-points); detail.swipe = swipe direction. Default action: write --gesture-progress = detail.snap, which settles with a CSS transition (--gesture-snap-duration / --gesture-snap-ease), then gesture-handler-snap. preventDefault() leaves values where the finger left them. Persist until next gesture. | | gesture-handler-snap | GestureHandlerSnapEvent (CustomEvent & { type: "gesture-handler-snap"; detail: { value: number; index: number }; bubbles: true; cancelable: true; composed: true }) | The settle transition reached the snap point (at once when there is nothing to animate; never when a new gesture interrupts it). detail = { value, index } into snap-points. Commit here when consumer CSS follows --gesture-progress until the end. | | gesture-handler-cancel | GestureHandlerCancelEvent (CustomEvent & { type: "gesture-handler-cancel"; detail: { x: number; y: number; dx: number; dy: number; vx: number; vy: number; progress: number; scale: number; rotate: number; pointers: number; } & { type: \| "pan" \| "pan-x" \| "pan-y" \| "pinch" \| "rotate" \| "swipe" \| "tap" \| "double-tap" \| "long-press" \| null; pointerType: string; distance: number; rangePx: number; durationMs: number; snap: number \| null; swipe: "left" \| "right" \| "up" \| "down" \| null; }; bubbles: true; cancelable: true; composed: true }) | Recognized gesture cut short: browser took pointer (pointercancel, usually native scroll), is-disabled set, or element left the document. detail = provision. No -end, no glide. | | gesture-handler-swipe | GestureHandlerSwipeEvent (CustomEvent & { type: "gesture-handler-swipe" \| gesture-handler-swipe-${"left" | "right" | "up" | "down"}; detail: { direction: "left" \| "right" \| "up" \| "down"; velocity: number }; bubbles: true; cancelable: true; composed: true }) | On release, velocity along dominant axis ≥ swipe-min-velocity and direction allowed by swipe-directions. detail = { direction, velocity } (px/ms). Direction twin fires next (gesture-handler-swipe-left / -right / -up / -down). | | gesture-handler-tap | GestureHandlerPointEvent (CustomEvent & { type: \| "gesture-handler-tap" \| "gesture-handler-double-tap" \| "gesture-handler-long-press"; detail: { x: number; y: number }; bubbles: true; cancelable: true; composed: true }) | Down and up without moving past threshold-px, within long-press-ms. detail = { x, y } relative to the element. | | gesture-handler-double-tap | GestureHandlerPointEvent (CustomEvent & { type: \| "gesture-handler-tap" \| "gesture-handler-double-tap" \| "gesture-handler-long-press"; detail: { x: number; y: number }; bubbles: true; cancelable: true; composed: true }) | Second tap within double-tap-ms of the previous (after its gesture-handler-tap). detail = { x, y }. Pair consumed; a third tap starts over. | | gesture-handler-long-press | GestureHandlerPointEvent (CustomEvent & { type: \| "gesture-handler-tap" \| "gesture-handler-double-tap" \| "gesture-handler-long-press"; detail: { x: number; y: number }; bubbles: true; cancelable: true; composed: true }) | Pointer stayed down without moving for long-press-ms. detail = { x, y }. |

CSS Custom Properties

| Name | Syntax | Default | Description | | --- | --- | --- | --- | | --gesture-x | <length> | 0px | Pointer centroid inside the element (px), horizontal — spotlight / parallax effects. Written every frame of a gesture; this is the resting value. | | --gesture-y | <length> | 0px | Pointer centroid inside the element (px), vertical. | | --gesture-dx | <length> | 0px | Horizontal travel since the gesture started (px) — drag-to-move, swipe-to-dismiss. | | --gesture-dy | <length> | 0px | Vertical travel since the gesture started (px). | | --gesture-vx | <number> | 0 | Horizontal velocity over the last ~100 ms (px/ms). | | --gesture-vy | <number> | 0 | Vertical velocity over the last ~100 ms (px/ms). | | --gesture-progress | <number> | 0 | Travel along progress-axis as a fraction of the range, clamped to progress-min..progress-max — the value a scrubbable sheet, carousel or slider follows. Persists after release until the next gesture. | | --gesture-range-px | <length> | 0px | The measured range (range-ref size along the axis, or range-px). Written once, when the gesture starts. | | --gesture-width | <length> | 0px | The element's own width, measured when the gesture starts. | | --gesture-height | <length> | 0px | The element's own height, measured when the gesture starts. | | --gesture-scale | <number> | 1 | Two-finger pinch ratio (1 = unchanged) — pinch-to-zoom. | | --gesture-rotate | <angle> | 0deg | Two-finger rotation — rotate-to-adjust. | | --gesture-pointers | <integer> | 0 | Pointers currently down. | | --gesture-distance | <length> | hypot(var(--gesture-dx), var(--gesture-dy)) | Length of the travel (px). Computed from --gesture-dx / --gesture-dy. | | --gesture-angle | <angle> | atan2(var(--gesture-dy), var(--gesture-dx)) | Direction of the travel: 0deg right, 90deg down. Computed from --gesture-dx / --gesture-dy. | | --gesture-progress-px | <length> | calc(var(--gesture-progress) * var(--gesture-range-px)) | --gesture-progress in px. Computed from --gesture-progress and --gesture-range-px. | | --gesture-x-ratio | <number> | tan(atan2(var(--gesture-x), var(--gesture-width))) | --gesture-x as a 0..1 fraction of the element's width. Computed from --gesture-x and --gesture-width. | | --gesture-y-ratio | <number> | tan(atan2(var(--gesture-y), var(--gesture-height))) | --gesture-y as a 0..1 fraction of the element's height. Computed from --gesture-y and --gesture-height. | | --gesture-snap-duration | <time> | 200ms | How long --gesture-progress takes to settle on its snap point after release. | | --gesture-snap-ease | <easing-function> | ease-out | Easing of that settle. |

CSS Aliases

| Alias | Kind | Matches | Description | | --- | --- | --- | --- | | :--gesture-handler | element | gesture-handler, .tag-gesture-handler | | | :--gesture-handler--is-active | state | [is-active], [data-active] | |

Examples

Swipeable carousel

slide-animation="track" turns content-carousel into a draggable track. progress-axis="left" makes a leftward drag pull the next slide in; the :has() rules narrow the bounds at the first and last slide so nothing wraps mid-drag; the gesture-handler-snap block swaps is-active and un-scrubs in one commit.

<gesture-handler gesture-types="pan-x swipe" pointer-types="touch pen mouse" progress-axis="left"
  range-ref=":scope > content-carousel" progress-min="-1" progress-max="1" snap-points="-1 0 1"
  overshoot-resistance="0.2">
  <quark-sheet>
    :scope {
      /* no wrap-around while dragging: clamp the ends */
      &:has(content-carousel-slide[is-active]:first-of-type) { progress-min: 0; }
      &:not(:has(content-carousel-slide[is-active]:first-of-type)) { progress-min: -1; }
      &:has(content-carousel-slide[is-active]:last-of-type) { progress-max: 0; }
      &:not(:has(content-carousel-slide[is-active]:last-of-type)) { progress-max: 1; }
      @on gesture-handler-start {
        content-carousel { is-scrubbing: ""; }
      }
      /* commit once the glide lands: the slide swap and the un-scrub paint together */
      @on gesture-handler-snap {
        /* snap-points "-1 0 1" → index 0 = previous, 1 = stay, 2 = next */
        $snap: event.detail.index;
        content-carousel {
          is-scrubbing: none;
          content-carousel-slide[is-active] + content-carousel-slide {
            is-active: $snap == 2 or preserve;
          }
          content-carousel-slide:has(+ content-carousel-slide[is-active]) {
            is-active: $snap == 0 or preserve;
          }
          content-carousel-slide[is-active] {
            is-active: $snap == 1 or none;
          }
        }
      }
    }
  </quark-sheet>
  <content-carousel slide-animation="track">
    <content-carousel-slide is-active>
      <h2>One</h2>
    </content-carousel-slide>
    <content-carousel-slide>
      <h2>Two</h2>
    </content-carousel-slide>
    <content-carousel-slide>
      <h2>Three</h2>
    </content-carousel-slide>
  </content-carousel>
</gesture-handler>

Pinch, rotate, drag

Two fingers (a trackpad or touch screen) for --gesture-scale and --gesture-rotate; one for --gesture-dx / --gesture-dy. Pure CSS mapping, no sheet. Each gesture starts from the resting values.

<gesture-handler gesture-types="pan pinch rotate" pointer-types="touch pen mouse">
  <figure>Drag, pinch, twist</figure>
  <style>
    #demo-gesture-handler-pinch > :first-child figure {
      translate: var(--gesture-dx, 0px) var(--gesture-dy, 0px);
      scale: var(--gesture-scale, 1);
      rotate: var(--gesture-rotate, 0deg);
    }
  </style>
</gesture-handler>

Swipe to dismiss

snap-points="-1 0 1" with a literal range-px: a flick past swipe-min-velocity snaps the card off to the side, and the gesture-handler-snap block records the fact. A tap starts a new gesture (values reset) and clears it.

<gesture-handler gesture-types="pan-x swipe tap" pointer-types="touch pen mouse" range-px="240" progress-min="-1" progress-max="1"
  snap-points="-1 0 1" overshoot-resistance="0.4">
  <quark-sheet>
    :scope {
      @on gesture-handler-snap {
        article { data-is-dismissed: event.detail.value != 0; }
      }
      @on gesture-handler-tap {
        article { data-is-dismissed: none; }
      }
    }
  </quark-sheet>
  <article>
    <h2>Swipe me away</h2>
    <p>Flick left or right. Tap to bring it back.</p>
  </article>
  <style>
    #demo-gesture-handler-swipe > :first-child article {
      translate: calc(var(--gesture-progress, 0) * 240px);
      rotate: calc(var(--gesture-progress, 0) * 8deg);
      &[data-is-dismissed] { opacity: 0.25; }
    }
  </style>
</gesture-handler>

Release notes

0.2.2 (2026-10-07)

  • Fix a type error in projects that load Node's timer types: the element's animation frame handle is typed as a number

0.2.1 (2026-10-01)

  • Pan handlers cancel the page's cross-axis scroll on the first touch move

0.2.0 (2026-09-30)

  • Hand a handoff-ref overscroll to the gesture only when every scroller between the pointer and the element is at its limit, so content scrolled inside a nested scroller scrolls back before the sheet moves

Older releases: https://nucleus.excom.dev/packages/gesture-handler


Full documentation: https://nucleus.excom.dev/packages/gesture-handler