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-curve

v0.1.1

Published

Curved / arc slider addon for Swiper: bend the strip along a dome, a bowl, a wave, a ramp or a full circle. 300 presets

Downloads

341

Readme

swiper-curve

An add-on for Swiper (v11+, tested on v14) that bends the strip of slides along a shape instead of laying it out flat: a dome, a bowl, a sine wave, a straight ramp - or a full circle. Every slide gets its place on the curve, a tangential rotation and - if you want it - a 3D turn, a push into the depth, a shrink, a fade or a blur towards the edges.

Zero dependencies beyond Swiper itself. One JS file plus one CSS file, plain CSS transforms, no canvas and no WebGL.

How it works

It is a regular Swiper module, the same shape as Navigation or Pagination. It registers its own parameters through extendParams({ curve: {...} }), 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, CurveEffect],   // sits beside the other modules
  slidesPerView: 'auto',
  navigation: { nextEl: '.next' },
  curve: { shape: 'arch', radius: 1400 },   // <- the plugin's parameters
});

Under the hood:

  1. In beforeInit the plugin turns on watchSlidesProgress (Swiper's own effects do the same) and adds a swiper-curve class to the container.
  2. On every setTranslate it reads each slide's swiperSlideOffset and the current swiper.translate, and works out how far that slide sits from the apex of the curve, in pixels. That is the crux: the plugin does not know Swiper's maths, it only reads the result. Which is why slidesPerView, slidesPerGroup, spaceBetween, loop, centeredSlides and freeMode work without it knowing anything about them.
  3. The distance goes through the shape function, which returns a displacement across the strip and the tangent at that point. The slide gets one transform: translate3d for the displacement, rotate for the tangent, plus optional rotateX / rotateY / Z push, and a scale.
  4. On a transition Swiper hands over the duration and the slides get the same one, so the arc animates together with the strip instead of snapping.
  5. With overflow: 'fit' (the default) the plugin also pads the container by exactly as much as the arc needs, including the extra height a rotated card takes up - so a bent strip never gets clipped and the layout reserves the room for it.

The types (src/swiper-curve.d.ts) add curve 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-curve
import Swiper from 'swiper';
import { FreeMode } from 'swiper/modules';
import 'swiper/css';
import { CurveEffect } from 'swiper-curve';
import 'swiper-curve/css';

new Swiper('.swiper', {
  modules: [CurveEffect, FreeMode],
  slidesPerView: 'auto',
  spaceBetween: 18,
  centeredSlides: true,
  grabCursor: true,
  freeMode: { enabled: true, momentumRatio: 0.7 },
  curve: {
    shape: 'arch',
    radius: 1400,
  },
});

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.

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

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

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

<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-curve@0/dist/swiper-curve.css">

<script src="https://cdn.jsdelivr.net/npm/swiper@14/swiper-bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/swiper-curve@0/dist/swiper-curve.iife.js"></script>
<script>
  new Swiper('.swiper', {
    modules: [SwiperCurveEffect],   // global from the IIFE build
    slidesPerView: 'auto',
    spaceBetween: 18,
    curve: { shape: 'arch', radius: 1400 },
  });
</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.

Shapes

| shape | What it does | |---|---| | 'arch' (default) | the middle rides high, the ends fall away. radius sets how tight the dome is - smaller means a stronger bend | | 'bowl' | the same circle mirrored: the middle dips, the ends climb | | 'wave' | a sine along the strip, driven by the wave group (amplitude, length, phase) instead of radius | | 'ramp' | a straight diagonal at ramp.angle degrees. No bend at all | | 'circle' | the strip closes into a full ring. See below |

strength multiplies the displacement of any of them: 1 is the exact geometric curve, 0 flattens it, 2 doubles it while keeping the shape.

origin moves the apex to 'left' or 'right' instead of the middle, so the strip rises out of a corner.

The full circle

shape: 'circle' is the only shape that also moves the slide along the strip: an arc length of d px lands at the chord position r * sin(d / r), so the row closes into a ring instead of running off the sides. The angle wraps, so a strip longer than the circumference simply keeps going round.

curve: {
  shape: 'circle',
  overflow: 'free',       // a ring is wider than its box
  rotate: { max: 0 },     // 0 = uncapped, a ring turns the whole way
  circle: {
    fit: true,            // radius derived from the strip length: one closed ring
    spin: 0,              // where the middle slide sits, in degrees. 0 = the top
  },
}

Two things decide whether it looks right:

  • Card size. With circle.fit the radius is stripLength / 2π, so ten 300px cards give a ring 950px across and twenty give 1900px. Small cards, or fewer of them, keep the ring inside a section.
  • Room around it. overflow: 'free' stops the plugin clipping the ring on either axis; 'fit' instead pads the container by the ring's radius, which keeps everything inside the box at the price of a very tall block.

strength squashes the ring into an ellipse (0.5 is half as tall as it is wide) and the rotation follows the squashed tangent. With rotate.enabled: false the cards stay upright while travelling round - a clock face rather than a wheel. edge.fade / edge.scale / edge.blur ramp with the angle here, so they dim the far side of the ring rather than the sides of the viewport.

Vertical room

A bent strip is taller than a flat one, so something has to give. That is what overflow decides:

| overflow | What it does | |---|---| | 'fit' (default) | the plugin pads the container so the whole arc stays inside its box. Nothing is clipped, the layout reserves the space | | 'spill' | the container is clipped along the strip only (clip-path), so the arc runs over the section above and below | | 'clip' | plain Swiper clipping: whatever leaves the box is cut off | | 'free' | nothing is clipped on either axis. What shape: 'circle' usually wants |

recenter: true (default) keeps the arc balanced in its box instead of letting it hang off one side.

Spacing

overflow buys room across the strip. The other axis needs it too: Swiper lays the strip out from the untransformed slide width, but a card turned by 45° eats noticeably more room along the strip than a card sitting flat. On a steep arch that is enough to run neighbours into each other, and no spaceBetween you pick by hand is right for every shape in the set.

So spacing.auto (on by default) works it out. It replays the strip at a candidate gap, tests neighbouring cards for real overlap, and finds the smallest gap that clears - which is a good deal tighter than the arithmetic suggests. A bounding box grows fast with rotation, but two cards turned by similar angles sit inside each other's boxes without ever touching. Measured on a deep bowl, the box arithmetic asked for 141px where 60px was enough, and those 81px cost three of the seven cards on screen.

spacing.gap is the gap you want left between two neighbours once the rotation is paid for. null, the default, means "whatever spaceBetween this carousel was built with" - so your own value stays the visual gap and is not overwritten. Move spaceBetween later and that becomes the new base.

Two cases sit it out. shape: 'circle' is left alone, because a ring already spreads its cards evenly and circle.fit reads the strip length to size itself, so the two would chase each other. And a layout that is meant to overlap - a deck, a fanned hand of cards - turns it off:

curve: { shape: 'arch', radius: 460, spacing: { auto: false } }

Fading the ends

mask puts a gradient over both ends of the container, so the strip fades out instead of ending on a hard cut:

curve: {
  mask: { enabled: true, width: 0.14 },   // 14% of the container at each end
}

This is the container fading, not the slides - it catches whatever is under it, including half-visible cards, which is what edge.fade cannot do (that one changes each slide's own opacity). The two combine happily.

The mask paints inside the container's box, so pair it with overflow: 'fit' or 'clip'. With 'spill' or 'free' the mask also cuts off whatever leaves the box across the strip, which is usually not what you meant.

sides: 'start' | 'end' fades only one end - handy when the row is anchored to one side of the page (origin: 'left') and should only dissolve into the other.

Depth of field

edge.blur is a ramp from the middle of the viewport: everything gets softer the further out it sits. dof is a lens - a focal plane you can move, a sharp zone around it, and a front/back bias:

curve: {
  dof: {
    enabled: true,
    focus: 0,        // 0 is the middle of the viewport, 1 the right-hand edge
    range: 0.14,     // sharp zone: 14% of a half-width either side of the focus
    blur: 16,        // px of blur once you are all the way out
    falloff: 2,      // exponent of the ramp outside the sharp zone
    dim: 0.25,       // out-of-focus cards also lose a quarter of their opacity
    bias: 1,         // and only the background blurs, like a real lens
  },
}

Distances are normalised to half the container, so focus and range read the same whatever the viewport is: focus: -0.5 puts the sharp card halfway between the middle and the left edge, range: 0 gives a single sharp plane.

bias is the photographic part: +1 leaves everything in front of the focal plane sharp and blurs only what is past it, -1 does the opposite, 0 treats both sides alike.

It stacks with edge: blur adds up, dimming and shrink multiply. The whole Depth of field preset family is built on it (dof-rack-focus, dof-eighty-five, dof-backdrop, dof-hyperfocal and twenty-one more), as are focus-aperture, focus-macro, focus-tilt-shift and arch-lens.

A word on cost. A CSS filter: blur() on a moving element repaints every frame. On a strip of eight or ten cards that is fine; on forty it is not. Keep dof.blur for carousels with a handful of slides on screen, or accept a lower frame rate on weak hardware.

Parameters

curve: {
  enabled: true,
  shape: 'arch',          // arch | bowl | wave | ramp | circle
  radius: 1400,           // px, smaller = tighter bend (arch / bowl / circle)
  strength: 1,            // multiplier on the displacement
  origin: 'center',       // center | left | right
  overflow: 'fit',        // fit | spill | clip | free
  recenter: true,
  respectReducedMotion: false,

  rotate: {
    enabled: true,        // sit each slide on the tangent
    amount: 1,            // multiplier over the tangent angle
    max: 30,              // cap in degrees, 0 = uncapped
  },

  spacing: {              // see "Spacing" below
    auto: true,           // widen spaceBetween by what the rotation costs
    gap: null,            // null = keep the spaceBetween you set as the gap
  },

  wave: {                 // shape: 'wave' only
    amplitude: 90,        // px
    length: 900,          // px per period
    phase: 0,             // 0..1 of a period
  },

  circle: {               // shape: 'circle' only
    fit: true,            // radius from the strip length, so the ring closes
    spin: 0,              // where the middle slide sits, degrees. 0 = the top
  },

  ramp: {                 // shape: 'ramp' only
    angle: 8,             // degrees, negative tilts the other way
  },

  depth: {                // 3D, all off by default
    perspective: 1400,    // px, on the container
    z: 0,                 // px pushed back at the edges
    tilt: 0,              // rotateX at the edges, degrees
    turn: 0,              // rotateY at the edges, degrees
  },

  mask: {                 // gradient fade over the ends of the container
    enabled: false,
    width: 0.12,          // width of each fade, 0..0.5 of the container
    sides: 'both',        // both | start | end
  },

  dof: {                  // depth of field
    enabled: false,
    focus: 0,             // focal plane along the strip, in half-widths
    range: 0.18,          // half-width of the sharp zone, same units
    blur: 12,             // px at the far end
    falloff: 1.6,         // how fast the blur ramps up outside the zone
    dim: 0.2,             // dimming that rides with the blur, 0..1
    scale: 0,             // shrink that rides with the blur, 0..1
    bias: 0,              // 0 both sides, 1 background only, -1 foreground only
  },

  edge: {                 // falloff towards the edge of the viewport
    scale: 0,             // 0..1 shrink
    fade: 0,              // 0..1 opacity loss
    blur: 0,              // px
    falloff: 1,           // exponent of the ramp
    stack: true,          // z-index by distance from the centre
  },
}

Runtime API

swiper.curve.setParams({ radius: 800 });          // live, from the next frame
swiper.curve.setParams({ rotate: { max: 45 } });  // deep-merged into the tree
swiper.curve.refresh();                           // after swapping slides by hand
swiper.curve.params;                              // current state

Every parameter is read fresh each frame, so anything in the tree is free to animate (from GSAP, a scroll handler or your own requestAnimationFrame).

Slide structure

Nothing special is required - ordinary Swiper slides:

<div class="swiper">
  <div class="swiper-wrapper">
    <div class="swiper-slide"><img src="card.jpg" alt=""></div>
    ...
  </div>
</div>

Two things to keep in mind:

  • The plugin writes transform, opacity, filter and z-index on the .swiper-slide elements. Do not animate those on the slides yourself, and do not combine this with Swiper's own 3D effects (effect: 'coverflow', 'cards', 'creative') - both would write to the same property. Put your own transforms on a child element inside the slide instead.
  • loop needs a healthy surplus of slides over slidesPerView. With too few Swiper quietly fails to build the loop blocks and the strip wedges at the end. Keep roughly twice slidesPerView in the DOM, or leave loop off.

The effect also works on a vertical Swiper: the curve then runs down the page and the displacement is horizontal.

Presets

Three hundred compositions live in src/curve-presets.js under the presets export, grouped into twelve families of twenty-five. 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 a repeated tree of numbers.

| Family | What drives it | |---|---| | Arch | the middle rides high and the ends fall away | | Bowl | the mirror image: the middle dips and the ends climb | | Wave | a sine along the strip, from a ripple to a full swell | | Wheel | the arc plus 3D: cards turn, recede and stack | | Editorial | small bends tuned for sections of real content | | Ramp | straight diagonals, no bend at all | | Hero | big statement curves for a landing section, most of them spilling over it | | Focus | the middle card is the subject: the rest shrinks, dims, blurs or steps back | | Marquee | shallow ribbons built to run on autoplay - these also set up the carousel | | Deck | a hand of cards: tight radius, heavy rotation, cards overlapping | | Depth of field | one card in focus, the rest defocused by a lens - these size the cards too, because blur costs a repaint per card | | Circle | the strip closed into a full ring - these size the cards too, because a ring only closes if the strip fits around it |

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

presets['arch-arch'].curve        // a ready config
presets['wheel-carousel'].curve
presetFamilies                    // [{ id, label, note, presets: [...] }]

A preset key is ${family}-${name}, e.g. bowl-hammock, wave-serpentine, editorial-footnote.

If you want a starting point, take arch-arch, bowl-bowl, wave-wave, wheel-wheel, editorial-editorial, ramp-ramp, hero-hero, focus-focus, marquee-marquee, deck-deck, dof-depth-of-field or circle-circle - those are the family bases and the rest are their variants.

An honest note: at 300 entries this is a catalogue, not a curation. A preset carrying a swiper block (the whole Marquee family, a couple elsewhere) also implies carousel options - autoplay, speed, card width - and the generated snippets include them.

Performance

  • One transform string per slide per frame, written only for the slides Swiper currently keeps in the DOM. No canvas, no layout reads beyond what Swiper has already measured.
  • edge.blur is the one expensive knob: a CSS filter on a moving element forces a repaint. Use it on a handful of cards, not on a 40-slide strip.
  • depth.* puts the container into a 3D context, which promotes the slides to their own layers - usually a win, but worth checking on old Android.
  • respectReducedMotion: true turns the whole effect off under prefers-reduced-motion: reduce and leaves an ordinary flat 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, so the page needs a network connection.

The site is a Vite app: npm run dev (port 3002) 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

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.

Structure

src/swiper-curve.js        the plugin (Swiper module + curve engine)
src/swiper-curve.css       required styles
src/curve-controls.js      the control schema behind the panel
src/curve-presets.js       300 presets in 12 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