SliderKit example

How it works

A slider of anything: pictures, cards, text or WebGL transitions, from one handler and one markup.

SliderKit moves a row of items with arrows, dots, touch or mouse drag, showing one or several at a time and a different number per breakpoint. It runs on three packages by the same author: @andresclua/sliderkit, the engine (layout, loop, touch, breakpoints, freeze), @andresclua/sliderkit-plugins, which adds the arrows, the pagination and the lifecycle hooks, and @andresclua/sliderkit-webgl, which replaces the slide transition with a GPU shader. The documentation and the repository cover the whole API; this page covers what the framework uses.

The class in src/js/handler/sliderkit maps six selectors to six configs: a standard slider, a text slider and four WebGL ones. Every config reads what it needs from data-* attributes on the element, so one handler serves every slider on the site. SliderKit.js is the class the Manager instantiates: it hands the options to the package and, when a config carries webglConfig, loads the WebGL plugin and the textures before building the slider.

The markup

A wrapper with the js--sliderkit hook and data-sliderkit-items, and one item per slide. The package wraps the element in its own .sliderkit__outer and .sliderkit__overflow, clones the items for the loop and injects the arrows and the dots. Its base SCSS is imported in style.scss; c--sliderkit-a adds the look of the items, the arrows and the dots. The SliderkitA component renders this markup from a slides prop.

<!-- The wrapper is the slider: the hook for the handler, and how many items are visible for the package -->
<div class="c--sliderkit-a">
  <div class="c--sliderkit-a__wrapper js--sliderkit" data-sliderkit-items="1">
    <div class="c--sliderkit-a__wrapper__item">…</div>
    <div class="c--sliderkit-a__wrapper__item">…</div>
    <div class="c--sliderkit-a__wrapper__item">…</div>
  </div>
</div>

The handler

The base config reads the number of visible items and the breakpoint overrides from the attributes, turns them into the package's responsive map and adds the three plugins. The loop clones the items when the slider mounts, and the Lazy handler only scans the DOM once, so the hooks plugin revalidates it on init and after every change, or the pictures in the clones would never load:

this.config = ({ element }) => {
    const responsive = {};
    const addBreakpoint = (width, attr) => {
        const value = parseInt(element.getAttribute(attr), 10);
        if (!isNaN(value)) responsive[width] = { items: value };
    };
    addBreakpoint(580,  "data-sliderkit-items-tablets");
    addBreakpoint(810,  "data-sliderkit-items-tabletm");
    addBreakpoint(1024, "data-sliderkit-items-tabletl");
    addBreakpoint(1300, "data-sliderkit-items-laptop");
    addBreakpoint(1570, "data-sliderkit-items-desktop");
    addBreakpoint(1700, "data-sliderkit-items-wide");

    const arrowsContainer = element.parentElement.querySelector(".js--sliderkit-arrows");

    return {
        items: parseInt(element.getAttribute("data-sliderkit-items")) || 1,
        loop: true,
        speed: 400,
        gutter: 32,
        mouseDrag: true,
        preventActionWhenRunning: true,
        responsive,
        plugins: [
            arrows(arrowsContainer ? { container: arrowsContainer } : {}),
            pagination({ type: "dots", clickable: true }),
            hooks({
                onInit: () => this.revalidateLazy(),
                afterChange: () => this.revalidateLazy(),
            }),
        ],
    };
};

What the framework does for you

  • Lazy loading. A standard or text slider is built when it gets within 30px of the viewport (Boostify, scroll method). The WebGL ones use a distance of 0: they preload textures and take the GPU, so they wait until they are in view. The package is imported once and cached by the Manager.
  • WebGL preload. SliderKit.js imports @andresclua/sliderkit-webgl on the first WebGL slider and keeps the module for the next pages. preloadWebGL turns the image paths into loaded HTMLImageElements, which the plugin needs before uploading them as textures; only then is the webgl plugin appended and the slider built:
if (this.webglConfig) {
    // Load once, reuse on subsequent page navigations
    if (!_webglModule) _webglModule = await import("@andresclua/sliderkit-webgl");
    const { webgl, preloadWebGL } = _webglModule;

    // Images must be loaded into HTMLImageElement before uploading as WebGL textures
    const assets = await preloadWebGL({
        slides:       this.webglConfig.slides,       // string[] — image paths
        displacement: this.webglConfig.displacement,  // string   — displacement map path
    });

    options.plugins = [
        ...(options.plugins || []),
        // effect: 'displacement' | 'custom' — see @andresclua/sliderkit-webgl docs
        webgl({
            effect: this.webglConfig.effect,
            assets,
            ...(this.webglConfig.frag     && { frag:     this.webglConfig.frag }),
            ...(this.webglConfig.duration !== undefined && { duration: this.webglConfig.duration }),
        }),
    ];
}
  • Page transitions. Every instance is destroyed before swup replaces the content, whichever of the six selectors is on the page, and rebuilt on the new one. SliderKit is registered with modifyHeight: true in resources.js.
  • Frozen arrows outside the slider. When a slider freezes (see Responsive and freeze), the package only hides the arrows inside its own wrapper. For arrows in a custom container, SliderKit.js reads slider.freeze on init and on every newBreakpointEnd and hides them itself.

Basic

One picture at a time, or three. The number of visible items is the only thing that changes.

Default items="1"items="3"

The default: data-sliderkit-items="1", arrows over the slides, dots under them, an infinite loop and mouse drag. The transition takes 400ms and, while it runs, further clicks and drags are ignored (preventActionWhenRunning). With data-sliderkit-items="3" three slides share the width, separated by the 32px gutter, and each step still moves one slide.

<!-- Three visible at once. Everything else is the same -->
<div class="c--sliderkit-a">
  <div class="c--sliderkit-a__wrapper js--sliderkit" data-sliderkit-items="3">
    <div class="c--sliderkit-a__wrapper__item">
      <img class="c--sliderkit-a__wrapper__item__media" src="…" alt="Slide 1" width="1600" height="900" loading="lazy">
    </div>
    …
  </div>
</div>
Live example
items="1"items="3"

One item

Slide 1
Slide 2
Slide 3
Slide 4
Slide 5

Three items

Slide 1
Slide 2
Slide 3
Slide 4
Slide 5

Responsive and freeze

A different number of items per breakpoint, and a slider that stops being one when everything fits.

Variation items="2"items-tabletl="4"freezable: true

data-sliderkit-items is the number of visible items from 0px up. Add a breakpoint suffix to change it from a width on: data-sliderkit-items-tablets (580px), -tabletm (810px), -tabletl (1024px), -laptop (1300px), -desktop (1570px) and -wide (1700px), the breakpoints of _vars.scss. The handler collects them into the package's responsive option, a map of min-width to options, and the package switches on resize. Each item here is a g--card-09 with a link; the track is a flex row, so cards with different amounts of text get the same height.

Freeze is what the package does when there is nothing to slide: with freezable on (the default), whenever the number of slides is less than or equal to the number of visible items the slider freezes. The outer wrapper gets sliderkit--frozen, the track stays at its first position, the base SCSS hides the arrows and the dots, and touch and drag do nothing. The items keep the width they would have in a running slider, so the layout does not jump. It is checked again at every breakpoint change, which gives three cases:

  • Six cards, 2 then 4 visible: a slider at every width.
  • Four cards, 2 then 4 visible: a slider on small screens, frozen from tabletl, where all four fit.
  • Two cards, 3 visible: frozen at every width, and each card keeps one third of the row.
<!-- 2 visible by default, 4 from tabletl (1024px). Each item is a g--card-09 -->
<div class="c--sliderkit-a">
  <div class="c--sliderkit-a__wrapper js--sliderkit" data-sliderkit-items="2" data-sliderkit-items-tabletl="4">
    <div class="c--sliderkit-a__wrapper__item">
      <div class="g--card-09">…</div>
    </div>
    …
  </div>
</div>
Live example
items="2"items-tabletl="4"freezable: true

Six cards, 2 then 4 visible

Auto Dealers

Coverage built around inventory, garage liability, and sales risk.

Repair Shops

Coverage built around inventory, garage liability, and sales risk.

Car Rentals

Coverage built around inventory, garage liability, and sales risk.

Towing

Coverage built around inventory, garage liability, and sales risk.

Body Shops

Coverage built around inventory, garage liability, and sales risk.

Parking

Coverage built around inventory, garage liability, and sales risk.

Four cards, 2 then 4 visible: frozen on desktop

Auto Dealers

Coverage built around inventory, garage liability, and sales risk.

Repair Shops

Coverage built around inventory, garage liability, and sales risk.

Car Rentals

Coverage built around inventory, garage liability, and sales risk.

Body Shops

Coverage built around inventory, garage liability, and sales risk.

Two cards, 3 visible: frozen, each one keeps a third

Auto Dealers

Coverage built around inventory, garage liability, and sales risk.

Repair Shops

Coverage built around inventory, garage liability, and sales risk. Protection for tools, equipment and customer vehicles while they are in your care, custody or control.

Custom arrows

The arrows rendered outside the slider, in a container of yours.

Variation js--sliderkit-arrowsarrows({ container })

By default the arrows plugin injects its two buttons into the package's outer wrapper, over the slides. Give it a container and it injects them there instead. The handler looks for a js--sliderkit-arrows element next to the slider, inside the same parent, and passes it on when it finds one; the base config already does this, so there is nothing to add in JS. In SliderkitA the container is c--sliderkit-a__ft, rendered after the slider by the customArrows prop, and the SCSS lays the arrows out in flow, aligned to the right, instead of absolutely positioned.

This example has four cards, 2 then 4 visible, so it freezes from tabletl: the external arrows disappear with it, thanks to the sync in SliderKit.js.

<!-- The arrows container is a sibling of the slider, after it. The handler finds it from the parent -->
<div class="c--sliderkit-a">
  <div class="c--sliderkit-a__wrapper js--sliderkit" data-sliderkit-items="2" data-sliderkit-items-tabletl="4">
    <div class="c--sliderkit-a__wrapper__item">…</div>
    …
  </div>
  <div class="c--sliderkit-a__ft js--sliderkit-arrows"></div>
</div>
Live example
js--sliderkit-arrowsarrows({ container })

Auto Dealers

Coverage built around inventory, garage liability, and sales risk.

Repair Shops

Coverage built around inventory, garage liability, and sales risk.

Car Rentals

Coverage built around inventory, garage liability, and sales risk.

Body Shops

Coverage built around inventory, garage liability, and sales risk.

Text

Slides of text with no slide motion: the package switches instantly and GSAP animates what is inside.

Variation js--sliderkit-textspeed: 0hooks

A second selector, js--sliderkit-text, mapped to configText. Arrows only, no dots, and speed: 0, so the package does not slide the track at all; the motion comes from the hooks plugin and GSAP. onInit receives the slides and animates the title and subtitle of the first one; beforeChange receives the incoming slide and the direction, and animates its content from below when going forward and from above when going back. clearProps removes the inline styles when each tween ends, so the slides are clean for the next pass.

<!-- Another hook, another config. The items carry a title and a subtitle for GSAP to animate -->
<div class="c--sliderkit-a">
  <div class="c--sliderkit-a__wrapper js--sliderkit-text" data-sliderkit-items="1">
    <div class="c--sliderkit-a__wrapper__item">
      <div class="c--sliderkit-a__wrapper__item__bd">
        <h3 class="c--sliderkit-a__wrapper__item__title">Slide one</h3>
        <p class="c--sliderkit-a__wrapper__item__subtitle">…</p>
      </div>
    </div>
    …
  </div>
</div>
this.configText = ({ element }) => ({
    items: parseInt(element.getAttribute("data-sliderkit-items")) || 1,
    loop: true,
    speed: 0,
    mouseDrag: true,
    preventActionWhenRunning: true,
    plugins: [
        arrows(),
        hooks({
            onInit({ slides }) {
                gsap.from(slides[0].querySelectorAll(CONTENT_SEL), {
                    y: 20, autoAlpha: 0, stagger: 0.1,
                    duration: 0.55, ease: 'power3.out', clearProps: 'all',
                });
            },
            beforeChange({ incoming, direction }) {
                const sign = direction === 'forward' ? 1 : -1;
                gsap.from(incoming.querySelectorAll(CONTENT_SEL), {
                    y: sign * 24, autoAlpha: 0, stagger: 0.1,
                    duration: 0.55, ease: 'power3.out', clearProps: 'all',
                });
            },
        }),
    ],
});
Live example
js--sliderkit-textspeed: 0hooks

Slide one

The title and the subtitle rise into place when the slide arrives.

Slide two

Going forward they come from below; going back, from above.

Slide three

The package swaps the slides instantly and GSAP does the motion.

WebGL

Pictures that transition through a shader: three built-in effects and one of your own.

Variation js--sliderkit-webglwebgl-effectwebgl-srcwebgl-displacement

The webgl plugin draws the slides on a canvas and blends one into the next with a fragment shader. The package asks for items: 1 and speed: 0, since the plugin owns the animation; the slides in the DOM only give the canvas its size, and the pictures come from data-webgl-src on each of them, in DOM order. Three attributes on the slider drive it:

  • data-webgl-effect: displacement (the default when missing), rgb-shift, radial or custom.
  • data-webgl-displacement: a greyscale map that warps both frames. Required by displacement, ignored by the others.
  • data-webgl-src, on each slide: the picture to upload as a texture.

Four selectors, four configs, all the same shape: items, loop, speed: 0, no drag, the arrows, and a webglConfig that SliderKit.js resolves into the plugin. configWebGL is the one that reads the attributes and, when the slider has a caption next to it, rewrites it on every indexChanged event with a GSAP timeline, taking the text from data-title:

<!-- The slider carries the effect and the displacement map; each slide carries its picture.
     The caption is a sibling: configWebGL finds its label and title from the parent and
     rewrites them on every indexChanged (data-title gives the text) -->
<div class="c--sliderkit-b">
  <div class="js--sliderkit-webgl" data-webgl-effect="displacement" data-webgl-displacement="/webgl/displacement.png">
    <div class="c--sliderkit-b__item" data-webgl-src="/webgl/slide-1.jpg" data-title="Iceland Church"></div>
    <div class="c--sliderkit-b__item" data-webgl-src="/webgl/slide-2.jpg" data-title="Mountain Road"></div>
    …
  </div>
  <div class="c--sliderkit-b__ft">
    <p class="c--sliderkit-b__ft__badge wgl-caption__label">01 / 05</p>
    <h3 class="c--sliderkit-b__ft__title wgl-caption__title">Iceland Church</h3>
  </div>
</div>

<!-- The other effects have their own hook and no caption. rgb-shift and radial need no map -->
<div class="c--sliderkit-b">
  <div class="js--sliderkit-webgl-rgb">
    <div class="c--sliderkit-b__item" data-webgl-src="/webgl/slide-1.jpg"></div>
    …
  </div>
</div>
this.configWebGL = ({ element }) => {
    const slideEls = [...element.querySelectorAll("[data-webgl-src]")];
    const label    = element.parentElement?.querySelector(".wgl-caption__label");
    const title    = element.parentElement?.querySelector(".wgl-caption__title");
    const titles   = slideEls.map(el => el.getAttribute("data-title") || "");
    const total    = String(titles.length).padStart(2, "0");

    const config = {
        items: 1,
        loop: true,
        speed: 0,
        mouseDrag: false,
        plugins: [arrows()],
        webglConfig: {
            slides:       slideEls.map(el => el.getAttribute("data-webgl-src")),
            displacement: element.getAttribute("data-webgl-displacement") || null,
            effect:       element.getAttribute("data-webgl-effect") || "displacement",
        },
    };

    if (label && title) {
        config.webglConfig.onIndexChanged = ({ displayIndex }) => {
            const idx = (displayIndex - 1) % titles.length;
            gsap.timeline()
                .to([label, title], { y: -10, opacity: 0, duration: 0.18, stagger: 0.04 })
                .call(() => {
                    label.textContent = `${String(idx + 1).padStart(2, "0")} / ${total}`;
                    title.textContent = titles[idx];
                })
                .fromTo([label, title],
                    { y: 14, opacity: 0 },
                    { y: 0, opacity: 1, duration: 0.32, stagger: 0.07, ease: "power2.out" });
        };
    }

    return config;
};

configWebGLRgb and configWebGLRadial only set effect: "rgb-shift" and effect: "radial". The custom one carries its own fragment shader in frag, with the uniforms the plugin binds for you: vUv (UV coordinates), u_from and u_to (the outgoing and incoming textures), u_progress (the eased progress, 0 to 1) and u_ar (the aspect ratio); u_disp too when a displacement map was preloaded. This one wipes from left to right with a boundary that waves along the Y axis, over 700ms instead of the plugin's 900:

this.configWebGLCustom = ({ element }) => {
    const slideEls = [...element.querySelectorAll("[data-webgl-src]")];
    return {
        items: 1,
        loop: true,
        speed: 0,
        mouseDrag: false,
        plugins: [arrows()],
        webglConfig: {
            slides: slideEls.map(el => el.getAttribute("data-webgl-src")),
            effect: "custom",
            frag: `
                precision mediump float;
                uniform sampler2D u_from;
                uniform sampler2D u_to;
                uniform float u_progress;
                varying vec2 vUv;

                void main() {
                    vec2 uv = vUv;
                    float soft = 0.2;
                    float boundary = mix(1.0 + soft, -soft, u_progress);
                    float wave = sin(uv.y * 12.0) * 0.04 * sin(u_progress * 3.14159);
                    float mask = smoothstep(boundary - soft, boundary + soft, uv.x + wave);
                    vec4 from = texture2D(u_from, uv);
                    vec4 to   = texture2D(u_to,   uv);
                    gl_FragColor = mix(from, to, mask);
                }
            `,
            duration: 700,
        },
    };
};
Live example
js--sliderkit-webglwebgl-effectwebgl-srcwebgl-displacement

displacement, with the caption

01 / 05

Iceland Church

rgb-shift

radial

custom: wave wipe

Options reference

The attributes the handler reads from the HTML, and the package options the configs set.

Reference

Attributes (HTML)

AttributeWhereWhat it does
data-sliderkit-itemsThe sliderVisible items from 0px up. Defaults to 1 when missing or not a number
data-sliderkit-items-tablets, -tabletm, -tabletl, -laptop, -desktop, -wideThe sliderVisible items from that breakpoint on: 580, 810, 1024, 1300, 1570 and 1700px. Each one becomes an entry of responsive
data-webgl-effectThe WebGL sliderdisplacement (default), rgb-shift, radial or custom. Read by configWebGL; the other WebGL configs fix their effect in JS
data-webgl-displacementThe WebGL sliderPath of the greyscale displacement map. Needed by displacement only
data-webgl-srcEach WebGL slidePath of the picture, uploaded as a texture. The slides are collected in DOM order
data-titleEach WebGL slideText written in the caption title by configWebGL when the slide becomes active

Slider options (@andresclua/sliderkit)

OptionPackage defaultWhat it does and what the configs set
items1Slides visible at once. From data-sliderkit-items; always 1 in the WebGL configs
loopfalseInfinite loop, done by cloning the slides. true in every config
speed300Transition duration in ms. 400 for the standard slider; 0 for text and WebGL, where the motion is GSAP's or the shader's
gutter0Gap between slides in px. 32 in the standard config
mouseDragfalseDrag with the mouse, besides touch. true for standard and text, false for WebGL
preventActionWhenRunningfalseIgnores clicks and drags while a transition runs. true for standard and text
responsive{}Options per min-width, e.g. { 1024: { items: 4 } }. Built from the breakpoint attributes
freezabletrueNot set by the configs: the package default. Freezes the slider when the slides are no more than the visible items
plugins[]The plugin instances, below

Plugins (@andresclua/sliderkit-plugins)

PluginOptions usedWhat it does
arrows()containerPrevious and next buttons. Injected into the outer wrapper, or into container (an element or a selector) when given: the handler passes the js--sliderkit-arrows sibling. Also accepts prevText, nextText and wrapper
pagination()type: "dots", clickable: trueDots under the slider, one per slide, that go to it on click. type can also be fraction or progress; container moves them
hooks()onInit, beforeChange, afterChangeLifecycle callbacks. onInit({ slides, slider }), beforeChange({ outgoing, incoming, direction }) and afterChange({ slide, direction }), with direction being forward or backward. The plugin also has onDragStart, onDragEnd, onResize and beforeDestroy

webglConfig (SliderKit.js)

Not a package option: an object of the handler that SliderKit.js turns into a preloadWebGL call and a webgl plugin from @andresclua/sliderkit-webgl.

KeyGoes toWhat it does
slidespreloadWebGLImage paths, one per slide in DOM order. From data-webgl-src
displacementpreloadWebGLPath of the displacement map, or null. From data-webgl-displacement
effectwebgldisplacement, rgb-shift, radial or custom. Required
fragwebglGLSL fragment shader source. Required with custom, passed only when set
durationwebglTransition length in ms. The plugin defaults to 900; passed only when set
onIndexChangedthe sliderBound to the indexChanged event after init, with { displayIndex } among its data. configWebGL uses it for the caption

The plugin also accepts intensity (default 0.08), easing and extra uniforms; SliderKit.js does not pass them yet.