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

swiper-dispersion

v0.1.3

Published

WebGL edge-dispersion, liquid-glass refraction and motion blur addon for Swiper

Readme

swiper-dispersion

An add-on for Swiper (v11+, tested on v14) that renders the slides through WebGL and applies an edge-dispersion effect at the viewport boundary: prismatic channel separation, directional blur, a wavy "liquid glass" warp, a glowing boundary and a smear that follows drag speed.

Zero dependencies beyond Swiper itself. One JS file plus one CSS file.

How it works

It is a regular Swiper module, the same shape as Navigation or Pagination. It registers its own parameters through extendParams({ dispersion: {...} }), so you configure it inside the normal Swiper config, next to slidesPerView or loop. There is no separate init API.

new Swiper('.swiper', {
  modules: [Navigation, DispersionEffect],   // sits beside the other modules
  slidesPerView: 'auto',
  navigation: { nextEl: '.next' },
  dispersion: { mode: 'glass' },             // <- the plugin's parameters
});

Under the hood:

  1. The plugin hooks the init / update / resize / destroy events and, on init, inserts a <canvas> into the Swiper container.
  2. It hides the DOM slides (opacity: 0) but leaves them in the layout. Swiper still measures positions, drag and keyboard keep working, screen readers see ordinary HTML, and links keep their hit testing.
  3. Every frame it reads getBoundingClientRect() of each layer (image, video, text, card background) and draws them as textured quads into an offscreen buffer. That is the crux: the plugin does not know Swiper's maths, it only reads the result from the DOM. That is why slidesPerView, slidesPerGroup, spaceBetween, loop, centeredSlides, freeMode, coverflow and the rest work without it knowing anything about them.
  4. Velocity comes from the difference in the strip's rendered position between frames (the wrapper's offset relative to the container), smoothed and passed to the shader as u_vel. Deliberately not from swiper.translate: while dragging that property follows the pointer, but on slideNext(), arrows or autoplay it jumps to the target value at once and the cards cover the rest of the distance with a CSS transform. Velocity read from it would be a single spike instead of accompanying the whole travel.
  5. A second pass measures each pixel's distance to the left and right edge and applies warp, turbulence, multi-sample spectral dispersion and the glowing band on that basis.

The types (src/swiper-dispersion.d.ts) add dispersion to SwiperOptions through declaration merging, so in a TypeScript project the field is suggested and type-checked inside a plain Swiper config.

Install

Bundler (Vite / webpack / Next)

npm i swiper swiper-dispersion
import Swiper from 'swiper';
import { FreeMode } from 'swiper/modules';
import 'swiper/css';
import { DispersionEffect } from 'swiper-dispersion';
import 'swiper-dispersion/css';

new Swiper('.swiper', {
  modules: [DispersionEffect, FreeMode],
  slidesPerView: 'auto',
  spaceBetween: 14,
  grabCursor: true,
  freeMode: { enabled: true, momentumRatio: 0.7 },
  dispersion: {
    background: '#ffffff',            // MUST match the section background
  },
});

Watch the modules list. Swiper has been modular since v9: options belonging to a module you did not register are silently ignored, with no warning. If you pass freeMode or autoplay without importing FreeMode / Autoplay from swiper/modules, they simply do nothing. This is not specific to this plugin, but it is the easiest way to conclude that the effect is broken when Swiper itself never enabled the behaviour.

Presets are imported separately so you do not pull them in when unused:

import { presets } from 'swiper-dispersion/controls';

From a CDN, no bundler (plain <script>)

jsDelivr and unpkg serve the package straight from npm, nothing has to be uploaded anywhere. The Swiper bundle registers every module, so freeMode and autoplay work without any extra imports here.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper@14/swiper-bundle.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swiper-dispersion@0/dist/swiper-dispersion.css">

<script src="https://cdn.jsdelivr.net/npm/swiper@14/swiper-bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swiper-dispersion@0/dist/swiper-dispersion.iife.js"></script>
<script>
  new Swiper('.swiper', {
    modules: [SwiperDispersionEffect],   // global from the IIFE build
    slidesPerView: 'auto',
    spaceBetween: 14,
    dispersion: { background: '#ffffff' },
  });
</script>

From a CDN as an ES module

<script type="module">
  import Swiper from 'https://cdn.jsdelivr.net/npm/swiper@14/swiper-bundle.min.mjs';
  import { DispersionEffect } from 'https://cdn.jsdelivr.net/npm/swiper-dispersion@0/+esm';

  new Swiper('.swiper', {
    modules: [DispersionEffect],
    slidesPerView: 'auto',
    dispersion: { background: '#ffffff' },
  });
</script>

In production pin an exact version (@0.1.0) instead of @0, so a new release cannot change how your page looks without warning.

The IIFE build is unminified (~40 kB, ~10 kB gzipped by the server). jsDelivr can minify on the fly if you add .min before the extension (swiper-dispersion.iife.min.js) - check that path after publishing, before you put it into production.

Required slide structure

<div class="swiper-slide">
  <img src="card.jpg" alt="">            <!-- card background -->
  <div class="card-title">Title</div>    <!-- text, rasterised into WebGL -->
  <div class="card-quote">"Quote..."</div>
</div>

The plugin collects from each slide:

| Element | What it does | |---|---| | the slide's background-color | the card fill | | img, video, canvas | texture, respects object-fit: cover / contain / fill | | leaf elements holding text | rasterised on a 2D canvas (font, colour, text-align, wrapping) | | the slide's border-radius | rounds and clips every layer |

Mind CORS: cross-origin images need crossorigin="anonymous" and an Access-Control-Allow-Origin header from the server. Without both, WebGL cannot upload the texture (the plugin logs a warning and skips the layer), and the canvas ends up tainted, which also kills toDataURL().

Text rasterisation supports a single style per element. If you need coloured fragments or gradient scrims, split them into separate elements or bake them into the image.

How many slides loop mode needs

Swiper's loop needs a healthy surplus of slides over slidesPerView. With too few it quietly fails to build the loop blocks and the strip wedges at the end: the arrows stop doing anything. Keep roughly twice slidesPerView in the DOM, or leave loop off.

Modes

| mode | What it does | |---|---| | 'glass' (default) | the full effect: edge zone, wavy boundary, glowing band and velocity smear | | 'motion' | motion blur only: no edge zone, no boundary, no warp. The carousel looks ordinary until you move it, and then gets directional smear with channel separation |

At rest, motion costs practically nothing (the shader exits after one texture fetch per pixel), so it suits a plain slider that should just blur nicely while moving:

dispersion: {
  mode: 'motion',
  motion: {
    amount: 120,        // smear length in px at full speed
    blur: 18,           // extra defocus blur
    spectrum: 0.5,      // how much channel split in the smear, 0 = clean blur
    sensitivity: 0.35,  // how quickly speed saturates the effect
    damping: 0.35,      // smoothing of the velocity signal, lower = softer
  },
}

The motion group is independent of edge, so you tune motion blur separately in glass mode too. In motion mode the remaining groups (edge, warp, aura) are ignored. motion.amount: 0 turns the smear off entirely.

Presets

A hundred and fifty compositions live in src/dispersion-presets.js under the presets export, grouped into six families of 25. Each family has a base that carries its character, and a single composition is that base plus a few explicit path-written deviations - so one line shows what a given preset changes, instead of 40 repeated numbers.

| Family | What drives it | |---|---| | Liquid glass | refraction with a glowing band at the zone boundary | | Clean edge | the same refraction with the band turned down to zero | | Motion only | velocity smear alone, zero cost at rest | | Editorial | restrained, tuned so the text stays readable | | Prismatic | led by colour, one hue per composition | | Noir | for dark sections, each sets its own background |

import { presets, presetFamilies } from 'swiper-dispersion/controls';

presets['glass-liquid-glass'].dispersion   // a ready config
presets['noir-obsidian'].dispersion
presetFamilies                             // [{ id, label, note, presets: [...] }]

A preset key is ${family}-${name}, e.g. motion-cinematic, prismatic-cobalt, editorial-footnote.

An honest note: at 150 entries this is a catalogue, not a curation. If you want a starting point, take glass-liquid-glass, clean-silk, motion-glide, editorial-editorial, prismatic-chromatic or noir-onyx - those are the family bases and the rest are their variants.

Image quality

A few decisions that make a visible difference and are not obvious:

  • Averaging in linear space. Blur computed directly on sRGB values eats highlights: a bright pixel smeared over a dark one lands much lower than where light actually mixes. The shader decodes samples to linear, averages, and encodes back. That is what makes the smear look lit rather than grey.
  • Sample count follows smear length. The step stays under ~2.5px, so the smear is continuous instead of breaking into visible copies. There is no sample jitter - that would trade banding for grain, and the goal here is clean glass.
  • No hard thresholds in the turbulence. A step() in noise gives glitch lines; here there are only smooth refraction bands.
  • Mipmaps plus anisotropic filtering on the card textures. Card artwork is usually larger than the space it is drawn into, so without mipmaps sharp cards shimmer.
  • Corner antialiasing computed in device px, not CSS px, so the rounding stays crisp on HiDPI screens.
  • Sub-1-LSB dithering on the output, so long smooth gradients do not band on an 8-bit buffer. The amplitude is invisible - this is not grain.
  • A text tail rather than copies. Text layers get one sharp copy and a series of fading ones, so it reads as movement instead of doubled glyphs.

Performance

  • Pixels outside the edge zone (the sharp middle cards, usually most of the canvas) skip the whole sampling loop: one texture fetch and done. Inside the edge zone the sample count scales with how far a given pixel actually travels, so you only pay full samples right at the edge.
  • The upper bound is samples samples per pixel. At samples: 28 on a 1440x420 canvas @2x that is about 34M samples per frame in the worst case - fine on desktop; on a weaker phone drop to samples: 12 and dpr: 1.5.
  • Rendering stops while the carousel is outside the viewport (pauseWhenHidden).
  • Video in slides is uploaded to the GPU every frame only while it is playing.
  • Under prefers-reduced-motion: reduce the effect does not start and CSS shows the ordinary DOM slides.

Fallback

If WebGL is unavailable the plugin logs a warning, does not add the swiper-dispersion-active class, and the carousel works as a plain DOM Swiper.

The site

index.html in the repository root is a landing page, documentation and playground in one: a live stage pinned to the top, a one-line entry per preset, a parameter table and a tuning panel. Selecting a preset loads it straight into the stage, and each entry hands you ready-to-paste code in three shapes: a whole HTML page, an npm import and CDN tags.

The demo cards use photos from Unsplash with crossorigin="anonymous", so the page needs a network connection.

The site is a Vite app: npm run dev (port 3001) and npm run build, which writes it to dist-site/. The published library build is a separate command, npm run build:lib, and that is what creates dist/.

Publishing to npm

npm publish        # prepublishOnly runs build.mjs on its own

Later versions through npm version patch|minor|major, then npm publish. Renaming a parameter or changing a default in a way that changes the look is minor before 1.0 and major after 1.0.

What ships (npm pack --dry-run): src/ (ESM, types and CSS), dist/ (the IIFE build plus CSS), README.md, LICENSE. The site sources under src/app/ and src/components/ stay in the repository only - files in package.json lists the library files explicitly rather than the whole src directory.

A few things in this package.json that matter:

  • peerDependencies: { "swiper": ">=11" } - the user installs Swiper, the package does not duplicate it.
  • exports with separate ./css, ./controls and ./iife paths, so import 'swiper-dispersion/css' works and internal files cannot be reached by accident.
  • sideEffects lists only the CSS and the IIFE build, so a bundler can drop unused code from the ESM module. swiper-dispersion.js itself writes nothing to window; the global name is attached by build.mjs.
  • types plus declaration merging in swiper-dispersion.d.ts make dispersion autocomplete inside a Swiper config with no extra setup on the user's side.

Structure

src/swiper-dispersion.js     the plugin (Swiper module + WebGL engine + shaders)
src/swiper-dispersion.css    required styles
src/dispersion-controls.js   the control schema behind the panel
src/dispersion-presets.js    150 presets in 6 families
build.mjs                    generates dist/ for <script> usage
index.html                   the site entry point (Vite)
src/app/                     demo site: React + Tailwind + shadcn/ui
  App.jsx                    top bar, hero, tabs, preset list
  Stage.jsx                  the live carousel (Swiper mounted in a ref)
  Panel.jsx                  the tuning panel shell
  panel.js                   panel controls built from the schema
  store.js                   effect state outside React + Swiper's life cycle
  favourites.js              starred presets, kept in localStorage
  artwork.js                 demo card photos and placeholder copy
src/components/ui/           shadcn/ui components

License

MIT