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

pinch-grid

v0.1.0

Published

Pinch-to-zoom grid density switching for mobile. Zero dependencies.

Downloads

225

Readme

pinch-grid

A tiny library for pinch-to-zoom grid density switching on mobile. Think iOS Photos, but for any product grid on the web.

Users spread two fingers to zoom into single-column view (bigger images, more detail). Pinch to zoom back out to multi-column. No dependencies, no framework lock-in, runs on raw touch events and CSS Grid.

~1.5KB gzipped. Zero dependencies. Works everywhere.

Live Demo (coming soon)


Why this exists

Mobile e-commerce is a tradeoff between density and detail. Two-column grids let shoppers scan more products. Single-column grids show bigger images and room for ratings, descriptions, quick-add buttons. Most stores pick one layout and stick with it.

This library lets the user decide. The interaction is intuitive because people already pinch-zoom on product images. Extending that gesture to the grid itself is a small leap that feels immediately obvious once you try it.

Install

npm install pinch-grid

Or grab the files directly. The entire library is two files: one JS module, one CSS file.

<link rel="stylesheet" href="pinch-grid.css">
<script type="module">
  import { init } from './pinch-grid.js';
  init('.product-grid');
</script>

Quick start

Add data-pinch-grid to your grid container. Tag any detail content you want hidden in compact mode with data-pg-detail.

<div class="product-grid" data-pinch-grid>
  <div class="product-card">
    <img src="product.jpg" alt="Product name">
    <h3>Product Name</h3>
    <p class="price">$49.99</p>

    <!-- This block only shows in single-column mode -->
    <div data-pg-detail>
      <p class="rating">4.8 stars (127 reviews)</p>
      <p class="description">Lightweight, durable, built to last.</p>
      <button>Add to Cart</button>
    </div>
  </div>
  <!-- more cards -->
</div>
import { init } from 'pinch-grid';

const grid = init('.product-grid', {
  columns: [1, 2],
  default: 2,
  persist: true
});

That's it. Pinch gestures work immediately on touch devices. Cards animate between layouts using FLIP transforms (GPU-composited, no layout thrashing). Scroll position stays anchored to whatever card the user was looking at.

How it works

The library does four things and tries to do them well:

1. Gesture detection. Native touchstart/touchmove listeners track two-finger distance. When the pinch ratio crosses a threshold (default 0.30), the column count switches. No gesture libraries, no pointer event abstraction layers. Just distance math on raw touches.

2. FLIP animation. Before switching columns, we snapshot every card's bounding rect. After the CSS Grid reflows to the new column count, we calculate the delta and animate each card from its old position to its new one using Element.animate() with transform: translate() scale(). Transforms stay on the compositor thread, so the animation runs at 60fps without triggering layout recalculations. A subtle stagger (15ms per card, capped at 60ms) gives it a cascading feel.

3. Content adaptation. Elements with data-pg-detail expand and collapse using grid-template-rows: 0fr to 1fr, the modern CSS technique for animating to/from zero height without the old max-height hack. In compact mode, detail content collapses smoothly. In single-column mode, it expands.

4. Scroll anchoring. Changing column count shifts the page layout. Without correction, the user loses their place. We find the card closest to the upper third of the viewport, record its offset, switch columns, then correct scroll position so that card stays visually anchored.

API

init(selector, options?)

Returns a PinchGrid instance.

| Option | Type | Default | Description | |--------|------|---------|-------------| | columns | number[] | [1, 2] | Available column counts, ordered ascending | | default | number | Last value in columns | Starting column count | | persist | boolean | true | Save preference to localStorage | | onSwitch | (cols: number) => void | null | Callback when columns change |

Instance methods

grid.setColumns(1);        // Switch to single column (with animation)
grid.getColumns();         // Returns current column count
grid.destroy();            // Remove listeners and clean up

CSS custom properties

The library sets these on the grid container. Override them for custom spacing:

[data-pinch-grid][data-pg-cols="1"] {
  --pg-gap: 24px;   /* default: 20px */
}

[data-pinch-grid][data-pg-cols="2"] {
  --pg-gap: 8px;    /* default: 12px */
}

Data attributes

| Attribute | Element | Purpose | |-----------|---------|---------| | data-pinch-grid | Grid container | Identifies the grid (added automatically if missing) | | data-pg-cols | Grid container | Current column count (set by JS, used by CSS) | | data-pg-detail | Any child element | Content visible only in single-column mode |

Platform integration

pinch-grid is framework-agnostic. It takes a selector or DOM element and handles everything internally. Here are integration notes for specific platforms.

Shopify (Liquid)

Drop the JS/CSS into your theme assets. Initialize on the collection template's product grid container. The data-pg-detail attribute works on any element inside your product card snippet.

<!-- sections/collection-template.liquid -->
<div id="product-grid" data-pinch-grid>
  {% for product in collection.products %}
    {% render 'product-card', product: product %}
  {% endfor %}
</div>

<script type="module">
  import { init } from '{{ "pinch-grid.js" | asset_url }}';
  init('#product-grid');
</script>

BigCommerce (Stencil)

Initialize in the category page's JS module. Target the existing product grid container in your Cornerstone/Starter/Roots theme.

// assets/js/theme/category.js
import { init } from './pinch-grid';

export default class Category extends PageManager {
  onReady() {
    if ('ontouchstart' in window) {
      init('.productGrid', {
        columns: [1, 2],
        default: 2,
        persist: true
      });
    }
  }
}

React

Wrap init in a useEffect. The ref ensures we target the right container.

import { useEffect, useRef } from 'react';
import { init } from 'pinch-grid';

function ProductGrid({ products }) {
  const gridRef = useRef(null);

  useEffect(() => {
    const pg = init(gridRef.current, { columns: [1, 2] });
    return () => pg?.destroy();
  }, []);

  return (
    <div ref={gridRef} data-pinch-grid>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </div>
  );
}

Vanilla HTML

Works out of the box. Include the CSS, import the module, call init. No build step required.

Configuration

Adding a toggle button

The library handles gestures, but you probably want a visible toggle for discoverability. Wire it up with setColumns:

const grid = init('.product-grid', {
  columns: [1, 2],
  onSwitch(cols) {
    document.querySelectorAll('.grid-toggle').forEach(btn => {
      btn.classList.toggle('active', parseInt(btn.dataset.cols) === cols);
    });
  }
});

document.querySelectorAll('.grid-toggle').forEach(btn => {
  btn.addEventListener('click', () => {
    grid.setColumns(parseInt(btn.dataset.cols));
  });
});

Three-column support (tablets)

Pass three values in the columns array. Pinch gestures cycle through them in order.

init('.product-grid', {
  columns: [1, 2, 3],
  default: 2
});

You'll need to add a CSS rule for the third state:

[data-pinch-grid][data-pg-cols="3"] {
  --pg-cols: 3;
  --pg-gap: 8px;
}

Disabling persistence

If you don't want the column preference saved across page loads:

init('.product-grid', { persist: false });

Performance

The library is designed to have zero measurable impact on page load and interaction metrics.

Bundle size: ~1.5KB gzipped (JS + CSS combined). No tree-shaking needed because there's nothing to shake.

Runtime cost: Two touch event listeners on the grid container (passive where possible, active only during two-finger gestures to prevent default zoom). No resize observers, no intersection observers, no mutation observers, no polling.

Animation: FLIP transforms run entirely on the compositor thread. No forced synchronous layouts during animation. will-change is set only during active transitions and removed immediately after.

Detail content: data-pg-detail visibility is pure CSS (grid-template-rows transition). No JS DOM manipulation for show/hide. No display: none toggling that would cause layout shifts.

Scroll anchoring: One getBoundingClientRect read per card before the switch, one scrollBy correction after. Runs once per gesture, not continuously.

What we intentionally don't do

No ResizeObserver for responsive breakpoints. If you need the grid to auto-switch at certain widths, handle that in your own CSS and call setColumns() from a media query listener. Bundling responsive logic into the library would bloat it and conflict with whatever responsive system your theme already uses.

No virtual scrolling or infinite scroll integration. pinch-grid handles layout density. Pair it with whatever pagination or infinite scroll solution you already have.

Project structure

pinch-grid/
  src/
    pinch-grid.js          # ES module, library core
    pinch-grid.css         # Required styles
  dist/
    pinch-grid.min.js      # Minified build
    pinch-grid.min.css     # Minified styles
    pinch-grid.iife.js     # Script-tag-friendly build
  demo/
    index.html             # Interactive demo page
  test/
    pinch-grid.test.js     # Unit tests
  package.json
  tsconfig.json            # Type declarations (source is JS, types via JSDoc)
  LICENSE
  README.md

Build

npm install
npm run build            # Produces dist/
npm run dev              # Serves demo with live reload
npm test                 # Runs test suite

The build uses esbuild for bundling (fast, no config bloat). Output targets:

  • dist/pinch-grid.min.js (ESM, tree-shakeable)
  • dist/pinch-grid.iife.js (global PinchGrid for script tags)
  • dist/pinch-grid.min.css

Browser support

Any browser that supports CSS Grid, Element.animate(), and touch events. In practice: Safari 13.1+, Chrome 67+, Firefox 63+, Edge 79+. That covers every mobile browser that matters.

Desktop browsers work fine with the toggle button API. Pinch gestures require a trackpad that supports multi-touch (most modern laptops do).

Accessibility

Column preference is persisted so users don't have to re-set it on every page. The hint pill that appears during pinch gestures is marked aria-hidden since it's purely visual feedback for an interaction that's already happening.

If you add toggle buttons (recommended), make sure they have appropriate aria-pressed or aria-current states. The library doesn't generate any UI elements except the transient hint overlay.

data-pg-detail content uses CSS visibility, not display: none, so screen readers can still access all product information regardless of the visual column count. Hidden detail content is collapsed to zero height but remains in the accessibility tree.

Roadmap

  • [x] TypeScript type declarations (.d.ts)
  • [ ] React wrapper hook (usePinchGrid)
  • [ ] Configurable animation duration and easing via options
  • [ ] columns: 'auto' mode that reads breakpoints from CSS
  • [ ] Three-column grid demo
  • [ ] Haptic feedback option via navigator.vibrate()
  • [ ] Published demo site

Contributing

Issues and PRs welcome. If you're integrating pinch-grid into a platform theme and run into edge cases, I'd like to hear about it.

Before submitting a PR:

  • Run npm test and make sure everything passes
  • Keep the library under 2KB gzipped. If a feature would push it over, it should be a separate module or plugin
  • No new dependencies. The zero-dep constraint is a feature, not a limitation

License

MIT