Displacement
A displacement map warps both frames at the peak of the transition, producing a fluid liquid-ink wipe.
View code
<div class="wgl-slide" data-slide-index="0"></div>
<div class="wgl-slide" data-slide-index="1"></div>
<div class="wgl-slide" data-slide-index="2"></div>
<div class="wgl-slide" data-slide-index="3"></div>
<div class="wgl-slide" data-slide-index="4"></div> .wgl-slide { height: 420px; background: #111; } .wgl-slide { height: 420px; background: #111; } import { Slider } from '@andresclua/sliderkit'
import { arrows } from '@andresclua/sliderkit-plugins'
import { webgl, preloadWebGL } from '@andresclua/sliderkit-webgl'
const assets = await preloadWebGL({
slides: [1,2,3,4,5].map(i => `/webgl/slide-${i}.jpg`),
displacement: '/webgl/displacement.png',
})
new Slider('#my-slider', {
items: 1, loop: true, speed: 0,
plugins: [arrows(), webgl({ effect: 'displacement', assets })],
}) RGB Shift
Chromatic aberration splits the R and B channels horizontally at mid-transition, then snaps back clean.
View code
<div class="wgl-slide" data-slide-index="0"></div>
<div class="wgl-slide" data-slide-index="1"></div>
<div class="wgl-slide" data-slide-index="2"></div>
<div class="wgl-slide" data-slide-index="3"></div>
<div class="wgl-slide" data-slide-index="4"></div> .wgl-slide { height: 420px; background: #111; } .wgl-slide { height: 420px; background: #111; } import { Slider } from '@andresclua/sliderkit'
import { arrows } from '@andresclua/sliderkit-plugins'
import { webgl, preloadWebGL } from '@andresclua/sliderkit-webgl'
const assets = await preloadWebGL({
slides: [1,2,3,4,5].map(i => `/webgl/slide-${i}.jpg`),
displacement: '/webgl/displacement.png',
})
new Slider('#my-slider', {
items: 1, loop: true, speed: 0,
plugins: [arrows(), webgl({ effect: 'rgb-shift', assets, intensity: 0.022 })],
}) Radial Reveal
Custom shader: the incoming image grows from the centre outward with a soft edge, like a spotlight opening.
View code
<div class="wgl-slide" data-slide-index="0"></div>
<div class="wgl-slide" data-slide-index="1"></div>
<div class="wgl-slide" data-slide-index="2"></div>
<div class="wgl-slide" data-slide-index="3"></div>
<div class="wgl-slide" data-slide-index="4"></div> .wgl-slide { height: 420px; background: #111; } .wgl-slide { height: 420px; background: #111; } import { Slider } from '@andresclua/sliderkit'
import { arrows } from '@andresclua/sliderkit-plugins'
import { webgl, preloadWebGL } from '@andresclua/sliderkit-webgl'
const assets = await preloadWebGL({
slides: [1,2,3,4,5].map(i => `/webgl/slide-${i}.jpg`),
displacement: '/webgl/displacement.png',
})
new Slider('#my-slider', {
items: 1, loop: true, speed: 0,
plugins: [arrows(), webgl({ effect: 'radial', assets })],
}) Wave Wipe (custom shader)
A sine-wave curtain sweeps left-to-right; the wave amplitude peaks at mid-transition and dies away cleanly. Written as a custom GLSL fragment shader passed directly to the plugin.
View code
<div class="wgl-slide" data-slide-index="0"></div>
<div class="wgl-slide" data-slide-index="1"></div>
<div class="wgl-slide" data-slide-index="2"></div>
<div class="wgl-slide" data-slide-index="3"></div>
<div class="wgl-slide" data-slide-index="4"></div> .wgl-slide { height: 420px; background: #111; } .wgl-slide { height: 420px; background: #111; } import { Slider } from '@andresclua/sliderkit'
import { arrows } from '@andresclua/sliderkit-plugins'
import { webgl, preloadWebGL } from '@andresclua/sliderkit-webgl'
const assets = await preloadWebGL({
slides: [1,2,3,4,5].map(i => `/webgl/slide-${i}.jpg`),
})
const FRAG_WAVE = `precision mediump float;
varying vec2 vUv;
uniform sampler2D u_from, u_to;
uniform float u_progress, u_ar;
vec2 cover(vec2 uv) {
float a = u_ar / 1.6;
vec2 r = vec2(min(a, 1.0), min(1.0 / a, 1.0));
return uv * r + (1.0 - r) * 0.5;
}
void main() {
float p = 1.04 - u_progress * 1.08;
float envelope = sin(u_progress * 3.14159);
float wave = p + sin(vUv.y * 18.0 + u_progress * 6.283) * 0.07 * envelope;
float t = smoothstep(wave - 0.04, wave + 0.04, vUv.x);
gl_FragColor = mix(texture2D(u_from, cover(vUv)), texture2D(u_to, cover(vUv)), t);
}`
new Slider('#my-slider', {
items: 1, loop: true, speed: 0,
plugins: [arrows(), webgl({ effect: 'custom', assets, frag: FRAG_WAVE })],
}) Text Overlay + GSAP
HTML lives above the WebGL canvas (z-index). On each slide change, GSAP animates the caption out, swaps the text, then animates it back in.
01 / 05
Iceland Church
View code
<div class="wgl-hero">
<div id="my-slider">
<div class="wgl-slide" data-slide-index="0"></div>
<div class="wgl-slide" data-slide-index="1"></div>
<div class="wgl-slide" data-slide-index="2"></div>
<div class="wgl-slide" data-slide-index="3"></div>
<div class="wgl-slide" data-slide-index="4"></div>
</div>
<div class="wgl-caption">
<p class="wgl-caption__label">01 / 05</p>
<h2 class="wgl-caption__title">Slide title</h2>
</div>
</div> .wgl-slide { height: 420px; background: #111; }
.wgl-hero { position: relative; }
.wgl-caption {
position: absolute; bottom: 0; left: 0; right: 0;
z-index: 10; pointer-events: none;
padding: 1.75rem 2rem;
background: linear-gradient(to top, rgba(0,0,0,.6) 0%, transparent 100%);
color: #fff;
}
.wgl-caption__label {
font-size: .7rem; text-transform: uppercase;
letter-spacing: .12em; opacity: .65; margin: 0 0 .3rem;
}
.wgl-caption__title { font-size: 1.6rem; font-weight: 700; margin: 0; } .wgl-slide {
height: 420px;
background: #111;
}
.wgl-hero {
position: relative;
}
.wgl-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
pointer-events: none;
padding: 1.75rem 2rem;
background: linear-gradient(to top, rgba(0,0,0,.6) 0%, transparent 100%);
color: #fff;
&__label {
font-size: .7rem;
text-transform: uppercase;
letter-spacing: .12em;
opacity: .65;
margin: 0 0 .3rem;
}
&__title {
font-size: 1.6rem;
font-weight: 700;
margin: 0;
}
} import gsap from 'gsap'
import { Slider } from '@andresclua/sliderkit'
import { arrows } from '@andresclua/sliderkit-plugins'
import { webgl, preloadWebGL } from '@andresclua/sliderkit-webgl'
const assets = await preloadWebGL({
slides: [1,2,3,4,5].map(i => `/webgl/slide-${i}.jpg`),
displacement: '/webgl/displacement.png',
})
const SLIDES = [
{ label: '01 / 05', title: 'Iceland Church' },
{ label: '02 / 05', title: 'Mountain Road' },
{ label: '03 / 05', title: 'Forest Path' },
{ label: '04 / 05', title: 'Car Interior' },
{ label: '05 / 05', title: 'Aerial View' },
]
const label = document.querySelector('.wgl-caption__label')
const title = document.querySelector('.wgl-caption__title')
const slider = new Slider('#my-slider', {
items: 1, loop: true, speed: 0,
plugins: [arrows(), webgl({ effect: 'displacement', assets })],
})
slider.on('indexChanged', ({ displayIndex }) => {
const s = SLIDES[(displayIndex - 1) % SLIDES.length]
gsap.timeline()
.to([label, title], { y: -10, opacity: 0, duration: 0.18, stagger: 0.04 })
.call(() => { label.textContent = s.label; title.textContent = s.title })
.fromTo([label, title],
{ y: 14, opacity: 0 },
{ y: 0, opacity: 1, duration: 0.32, stagger: 0.07, ease: 'power2.out' })
}) HTML-driven slides (accessible)
Images and text live in the HTML: data-image feeds the WebGL preloader, img provides the a11y fallback, and the text spans are sr-only so screen readers read them while GSAP drives the visual overlay.
01 / 05
Iceland Church
View code
<div class="wgl-hero">
<div id="my-slider">
<div class="wgl-slide" data-slide-index="0" data-image="/webgl/slide-1.jpg">
<img src="/webgl/slide-1.jpg" alt="Iceland church with mountains" class="wgl-slide__img" />
<span class="wgl-slide__label">01 / 05</span>
<span class="wgl-slide__title">Iceland Church</span>
</div>
<div class="wgl-slide" data-slide-index="1" data-image="/webgl/slide-2.jpg">
<img src="/webgl/slide-2.jpg" alt="Mountain road" class="wgl-slide__img" />
<span class="wgl-slide__label">02 / 05</span>
<span class="wgl-slide__title">Mountain Road</span>
</div>
<!-- … repeat for remaining slides … -->
</div>
<div class="wgl-caption" aria-live="polite">
<p class="wgl-caption__label">01 / 05</p>
<h2 class="wgl-caption__title">Iceland Church</h2>
</div>
</div> .wgl-slide {
height: 420px; background: #111;
position: relative; overflow: hidden;
}
.wgl-slide__img {
position: absolute; inset: 0;
width: 100%; height: 100%; object-fit: cover;
}
/* sr-only: text lives in DOM for a11y, canvas covers it visually */
.wgl-slide__label,
.wgl-slide__title {
position: absolute; width: 1px; height: 1px;
overflow: hidden; clip: rect(0,0,0,0);
white-space: nowrap;
}
.wgl-hero { position: relative; }
.wgl-caption {
position: absolute; bottom: 0; left: 0; right: 0;
z-index: 10; pointer-events: none;
padding: 1.75rem 2rem;
background: linear-gradient(to top, rgba(0,0,0,.6) 0%, transparent 100%);
color: #fff;
}
.wgl-caption__label {
font-size: .7rem; text-transform: uppercase;
letter-spacing: .12em; opacity: .65; margin: 0 0 .3rem;
}
.wgl-caption__title { font-size: 1.6rem; font-weight: 700; margin: 0; } .wgl-hero {
position: relative;
}
.wgl-slide {
height: 420px;
background: #111;
position: relative;
overflow: hidden;
&__img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
&__label,
.wgl-slide__title {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
}
}
.wgl-caption {
position: absolute;
bottom: 0;
left: 0;
right: 0;
z-index: 10;
pointer-events: none;
padding: 1.75rem 2rem;
background: linear-gradient(to top, rgba(0,0,0,.6) 0%, transparent 100%);
color: #fff;
&__label {
font-size: .7rem;
text-transform: uppercase;
letter-spacing: .12em;
opacity: .65;
margin: 0 0 .3rem;
}
&__title {
font-size: 1.6rem;
font-weight: 700;
margin: 0;
}
} import gsap from 'gsap'
import { Slider } from '@andresclua/sliderkit'
import { arrows } from '@andresclua/sliderkit-plugins'
import { webgl, preloadWebGL } from '@andresclua/sliderkit-webgl'
// images and text come from the DOM — no hardcoded arrays
const slideEls = [...document.querySelectorAll('#my-slider .wgl-slide')]
const assets = await preloadWebGL({
slides: slideEls.map(el => el.dataset.image),
displacement: '/webgl/displacement.png',
})
const wrap = document.querySelector('#my-slider').closest('.wgl-hero')
const label = wrap.querySelector('.wgl-caption__label')
const title = wrap.querySelector('.wgl-caption__title')
const slider = new Slider('#my-slider', {
items: 1, loop: true, speed: 0,
plugins: [arrows(), webgl({ effect: 'radial', assets })],
})
let prev = 1
slider.on('indexChanged', ({ displayIndex }) => {
const el = slideEls[(displayIndex - 1) % slideEls.length]
const total = slideEls.length
const diff = displayIndex - prev
const dir = Math.abs(diff) > total / 2 ? -Math.sign(diff) : Math.sign(diff)
prev = displayIndex
gsap.timeline()
.to(label, { x: -20 * dir, opacity: 0, duration: 0.15 })
.to(title, { y: -16 * dir, opacity: 0, duration: 0.2 }, '<0.04')
.call(() => {
label.textContent = el.querySelector('.wgl-slide__label').textContent
title.textContent = el.querySelector('.wgl-slide__title').textContent
})
.fromTo(label, { x: 20 * dir, opacity: 0 }, { x: 0, opacity: 1, duration: 0.28, ease: 'power2.out' })
.fromTo(title, { y: 18 * dir, opacity: 0 }, { y: 0, opacity: 1, duration: 0.38, ease: 'expo.out' }, '<0.06')
})