smooth plugin

Drag that eases in and out. The slider follows your pointer with a lerp, keeps its momentum when you let go, and settles on the nearest slide. Horizontal trackpad scroll gets the same treatment.

Usage

import { Slider } from '@andresclua/sliderkit'
import { smooth, arrows } from '@andresclua/sliderkit-plugins'

new Slider('#el', {
  items: 3,
  loop: true,
  touch: false,       // smooth() replaces the core drag
  mouseDrag: false,
  plugins: [smooth(), arrows()],
})

Presets

Start from a named behaviour and override whatever you like. Every other option wins over the preset.

smooth({ preset: 'snappy' })
smooth({ preset: 'floaty', inertia: 1200, skew: true })
PresetFeelValues
'smooth' (default)Soft lag, generous glide, always settles on a slide.lerp 0.1 · inertia 350 · snap
'snappy'Quick and controlled. A fling never travels more than one slide.lerp 0.22 · inertia 120 · snap · maxFling 1
'floaty'Heavy and dreamy. A lot of lag and a long glide.lerp 0.05 · inertia 700 · snap
'free'No snapping. It stops wherever the momentum runs out.lerp 0.08 · inertia 900 · no snap · maxFling 8

The presets are exported as a plain object, so you can read them, spread them or build your own: smooth({ ...smoothPresets.floaty, lerp: 0.04 }).

Options

OptionTypeDefaultDescription
preset'smooth' | 'snappy' | 'floaty' | 'free''smooth'Starting point. See above.
lerpnumberpresetFraction of the remaining distance covered per 60 fps frame. Lower is floatier, 1 is no smoothing. Frame-rate independent.
inertianumberpresetMilliseconds of release velocity projected forward. 0 disables the fling.
snapbooleanpresetSettle on a slide after a drag or wheel gesture.
maxFlingnumber2 × visible itemsMost slides a single fling can travel.
dragSpeednumber1Multiplier for pointer and wheel movement.
wheelbooleantrueHorizontal wheel / trackpad scroll. Vertical scroll is left alone so the page still scrolls.
wheelSpeednumber1Extra multiplier for the wheel only, on top of dragSpeed.
snapDelaynumber140Milliseconds of wheel silence before it settles on a slide.
dragThresholdnumber5Pixels of movement after which a drag stops counting as a click, so it doesn't open the link under the pointer.
skewboolean | objectfalseLean the slides into the movement. true uses the defaults below.
onUpdate(plugin) => void—Called every frame the slider moves, and once more when it comes to rest. Receives the plugin, so you can read velocity, position, progress and positions. See below.
rafbooleantrueRun its own animation loop. Set false to call plugin.tick(now) from your own requestAnimationFrame, so it moves in the same frame as other things you draw (a canvas, for example).

Skew

Slides lean in the direction they move, in proportion to the speed, and ease back to straight when they stop.

smooth({
  skew: {
    target: '.card',   // selector inside each slide (default: the slide itself)
    perSecond: 2.5,    // degrees per slide/second of velocity
    max: 14,           // max degrees either way
    ease: 0.2,         // how fast it returns to 0 (0–1 per frame)
  },
})

It sets transform: skewX() on the target, so use an element that has no transform of its own. For any other effect, read velocity yourself (next section).

onUpdate and live state

onUpdate is the extension point: the plugin gives you the numbers, you decide what to do with them. The skew option is just a ready-made use of it.

const slider = new Slider('#el', {
  touch: false, mouseDrag: false,
  plugins: [
    smooth({
      onUpdate(sm) {
        // runs every frame the slider moves, and once more when it stops (velocity 0)
        const scale = 1 - Math.min(Math.abs(sm.velocity) * 0.02, 0.1)
        cards.forEach(card => card.style.transform = `scale(${scale})`)
      },
    }),
  ],
})
MemberTypeDescription
velocitynumberSigned speed in slides per second, eased so it ramps up and decays smoothly (it never jumps when you let go). Positive towards the next slide, negative towards the previous one, 0 at rest.
positionnumberWhere the slider is, in slides from the first one. Wraps around when looping.
progressnumber0–1 along the slider.
positionsreadonly number[]One value per real slide. 0 = at the start of the viewport, 1 = one slide further right, negative = before it. With loop: true it wraps inside [-1, n-1), so every slide that can be on screen has a continuous value. Loop clones show the same slide, so they share its value.
draggingbooleantrue while a pointer is holding the slider.
tick(now)(number) => voidAdvances the animation by one frame. Only needed with raf: false.

Tip: build effects from continuous functions of the values (distance to a point, multiplied by a factor). Avoid Math.round, % or anything that steps: when the slider settles it crosses those thresholds and the effect jumps.

The same properties are readable at any time from the plugin you created (const sm = smooth()), for example from your own requestAnimationFrame. positions is reused between frames: copy it if you keep it.

Drawing on a canvas

If something else renders every frame (a WebGL canvas, for instance), create the plugin with smooth({ raf: false }) and call sm.tick(now) at the start of your own requestAnimationFrame callback, before you read the slides' positions. That way the slider and the canvas always update in the same frame and never drift apart. The home page does exactly this.

How it works

  1. The plugin keeps a continuous position (in slides) and a target. Every frame the position moves a fraction of the way to the target, and the result is written as the track's transform.
  2. Dragging moves the target. On release, the pointer velocity of the last 100 ms is projected forward (inertia) and the target snaps to a slide.
  3. The core stays in charge of the index: it is told where you landed, so arrows, pagination, a11y and events keep working. Clicking an arrow animates there by the shortest way round.
  4. With loop: true the position wraps over the clones the core already creates, so there is no jump at the seam.

Notes

  • Horizontal axis only.
  • Pass touch: false and mouseDrag: false, otherwise both drag systems fight.
  • The plugin disables the core's CSS transition on the track (it would lag behind the per-frame movement). speed no longer affects the slide movement.
  • With loop: false the movement stops hard at the ends (no rubber-band).
  • A drag never ends in a click on a link or button under the pointer.

Demos

  • Smooth drag demos — the plugin on its own, a skew boilerplate you can copy, your own effect with onUpdate, and the four presets side by side.
  • Home page — the same slider with a WebGL canvas that follows the slides.