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.jsimports@andresclua/sliderkit-webglon the first WebGL slider and keeps the module for the next pages.preloadWebGLturns the image paths into loadedHTMLImageElements, which the plugin needs before uploading them as textures; only then is thewebglplugin 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: trueinresources.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.jsreadsslider.freezeon init and on everynewBreakpointEndand hides them itself.
Basic
One picture at a time, or three. The number of visible items is the only thing that changes.
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> items="1"items="3" One item
Three items
Responsive and freeze
A different number of items per breakpoint, and a slider that stops being one when everything fits.
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> items="2"items-tabletl="4"freezable: true Six cards, 2 then 4 visible
Four cards, 2 then 4 visible: frozen on desktop
Two cards, 3 visible: frozen, each one keeps a third
Custom arrows
The arrows rendered outside the slider, in a container of yours.
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> js--sliderkit-arrowsarrows({ container }) Text
Slides of text with no slide motion: the package switches instantly and GSAP animates what is inside.
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',
});
},
}),
],
}); js--sliderkit-textspeed: 0hooks WebGL
Pictures that transition through a shader: three built-in effects and one of your own.
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,radialorcustom.data-webgl-displacement: a greyscale map that warps both frames. Required bydisplacement, 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,
},
};
}; js--sliderkit-webglwebgl-effectwebgl-srcwebgl-displacement displacement, with the caption
rgb-shift
radial
custom: wave wipe
Options reference
The attributes the handler reads from the HTML, and the package options the configs set.
Attributes (HTML)
| Attribute | Where | What it does |
|---|---|---|
data-sliderkit-items | The slider | Visible items from 0px up. Defaults to 1 when missing or not a number |
data-sliderkit-items-tablets, -tabletm, -tabletl, -laptop, -desktop, -wide | The slider | Visible items from that breakpoint on: 580, 810, 1024, 1300, 1570 and 1700px. Each one becomes an entry of responsive |
data-webgl-effect | The WebGL slider | displacement (default), rgb-shift, radial or custom. Read by configWebGL; the other WebGL configs fix their effect in JS |
data-webgl-displacement | The WebGL slider | Path of the greyscale displacement map. Needed by displacement only |
data-webgl-src | Each WebGL slide | Path of the picture, uploaded as a texture. The slides are collected in DOM order |
data-title | Each WebGL slide | Text written in the caption title by configWebGL when the slide becomes active |
Slider options (@andresclua/sliderkit)
| Option | Package default | What it does and what the configs set |
|---|---|---|
items | 1 | Slides visible at once. From data-sliderkit-items; always 1 in the WebGL configs |
loop | false | Infinite loop, done by cloning the slides. true in every config |
speed | 300 | Transition duration in ms. 400 for the standard slider; 0 for text and WebGL, where the motion is GSAP's or the shader's |
gutter | 0 | Gap between slides in px. 32 in the standard config |
mouseDrag | false | Drag with the mouse, besides touch. true for standard and text, false for WebGL |
preventActionWhenRunning | false | Ignores 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 |
freezable | true | Not 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)
| Plugin | Options used | What it does |
|---|---|---|
arrows() | container | Previous 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: true | Dots 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, afterChange | Lifecycle 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.
| Key | Goes to | What it does |
|---|---|---|
slides | preloadWebGL | Image paths, one per slide in DOM order. From data-webgl-src |
displacement | preloadWebGL | Path of the displacement map, or null. From data-webgl-displacement |
effect | webgl | displacement, rgb-shift, radial or custom. Required |
frag | webgl | GLSL fragment shader source. Required with custom, passed only when set |
duration | webgl | Transition length in ms. The plugin defaults to 900; passed only when set |
onIndexChanged | the slider | Bound 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.