Text reveal via beforeSlideChange / afterSlideChange

Each slide contains a real background image plus a text block with an eyebrow label, title, and description. beforeSlideChange animates the current text out (staggered, upward). afterSlideChange animates the incoming text in from below. GSAP handles all timing — the slider core only fires the events.

Architecture

Built for the modern web

Lightweight, accessible, and dependency-free at its core.

Performance

Zero layout thrash

Transforms only — no reflow on every frame.

Plugins

Extend without the weight

Import only the plugins you need — tree-shaking does the rest.

WebGL

GPU-powered transitions

GLSL shaders wired directly to the slide-change lifecycle.

View code
<div class="hero-slider" id="my-slider">
  <div class="c--slider-a__wrapper">

    <div class="c--slider-a__slide hs-slide" data-slide
         style="background-image:url('https://picsum.photos/seed/hs1/900/420')">
      <div class="hs-content">
        <span class="hs-tag">Architecture</span>
        <h2 class="hs-title">Built for the modern web</h2>
        <p class="hs-desc">Lightweight, accessible, and dependency-free at its core.</p>
      </div>
    </div>

    <div class="c--slider-a__slide hs-slide" data-slide
         style="background-image:url('https://picsum.photos/seed/hs2/900/420')">
      <div class="hs-content">
        <span class="hs-tag">Performance</span>
        <h2 class="hs-title">Zero layout thrash</h2>
        <p class="hs-desc">Transforms only — no reflow on every frame.</p>
      </div>
    </div>

    <div class="c--slider-a__slide hs-slide" data-slide
         style="background-image:url('https://picsum.photos/seed/hs3/900/420')">
      <div class="hs-content">
        <span class="hs-tag">Plugins</span>
        <h2 class="hs-title">Extend without the weight</h2>
        <p class="hs-desc">Import only the plugins you need — tree-shaking does the rest.</p>
      </div>
    </div>

    <div class="c--slider-a__slide hs-slide" data-slide
         style="background-image:url('https://picsum.photos/seed/hs4/900/420')">
      <div class="hs-content">
        <span class="hs-tag">WebGL</span>
        <h2 class="hs-title">GPU-powered transitions</h2>
        <p class="hs-desc">GLSL shaders wired directly to the slide-change lifecycle.</p>
      </div>
    </div>

  </div>
</div>
<div id="my-pag" class="c--slider-a__pagination"></div>
.hero-slider {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: 8px;
}
.c--slider-a__wrapper { display: flex; will-change: transform; }

.hs-slide {
  flex-shrink: 0;
  width: 100%;
  height: 380px;
  background-size: cover;
  background-position: center;
  position: relative;
}

/* Dark gradient so text is readable */
.hs-slide::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.1) 60%);
  border-radius: inherit;
}

.hs-content {
  position: absolute;
  bottom: 40px;
  left: 40px;
  right: 40px;
  z-index: 2;
  color: #fff;
}
.hs-tag {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  background: rgba(108, 43, 217, 0.85);
  padding: 3px 10px;
  border-radius: 99px;
  margin-bottom: 12px;
}
.hs-title {
  font-size: clamp(1.4rem, 3vw, 2rem);
  font-weight: 700;
  line-height: 1.2;
  margin-bottom: 8px;
}
.hs-desc {
  font-size: 0.95rem;
  opacity: 0.85;
  max-width: 480px;
}

/* Pagination dots */
.c--slider-a__pagination { display: flex; justify-content: center; padding: 12px 0; gap: 6px; }
.c--slider-a__pagination-bullet { width: 10px; height: 10px; border-radius: 50%; background: #d1d5db; border: none; cursor: pointer; transition: background 0.2s; }
.c--slider-a__pagination-bullet--active { background: #6C2BD9; }

/* Arrows */
.c--slider-a__arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 10; width: 40px; height: 40px; border-radius: 50%;
  border: none; background: rgba(255,255,255,0.15); backdrop-filter: blur(4px);
  cursor: pointer; color: #fff;
}
.c--slider-a__arrow--prev { left: 16px; }
.c--slider-a__arrow--next { right: 16px; }
.hero-slider {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: 8px;
}
.c--slider-a__wrapper { display: flex; will-change: transform; }

.hs-slide {
  flex-shrink: 0;
  width: 100%;
  height: 380px;
  background-size: cover;
  background-position: center;
  position: relative;
}

/* Dark gradient so text is readable */
.hs-slide::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.1) 60%);
  border-radius: inherit;
}

.hs-content {
  position: absolute;
  bottom: 40px;
  left: 40px;
  right: 40px;
  z-index: 2;
  color: #fff;
}
.hs-tag {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  background: rgba(108, 43, 217, 0.85);
  padding: 3px 10px;
  border-radius: 99px;
  margin-bottom: 12px;
}
.hs-title {
  font-size: clamp(1.4rem, 3vw, 2rem);
  font-weight: 700;
  line-height: 1.2;
  margin-bottom: 8px;
}
.hs-desc {
  font-size: 0.95rem;
  opacity: 0.85;
  max-width: 480px;
}

/* Pagination dots */
.c--slider-a__pagination { display: flex; justify-content: center; padding: 12px 0; gap: 6px; }
.c--slider-a__pagination-bullet { width: 10px; height: 10px; border-radius: 50%; background: #d1d5db; border: none; cursor: pointer; transition: background 0.2s; }
.c--slider-a__pagination-bullet--active { background: #6C2BD9; }

/* Arrows */
.c--slider-a__arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 10; width: 40px; height: 40px; border-radius: 50%;
  border: none; background: rgba(255,255,255,0.15); backdrop-filter: blur(4px);
  cursor: pointer; color: #fff;
}
.c--slider-a__arrow--prev { left: 16px; }
.c--slider-a__arrow--next { right: 16px; }
import { Slider } from '@andresclua/sliderkit'
import { arrows, pagination } from '@andresclua/sliderkit-plugins'
import { gsap } from 'gsap'

const slider = new Slider('#my-slider', {
  loop: true,
  speed: 600,
  plugins: [
    arrows(),
    pagination({ el: '#my-pag', type: 'dots', clickable: true }),
  ],
})

// Collect text containers from real slides only (not clones)
const contents = [...slider.slides].map(s => s.querySelector('.hs-content'))

// Animate initial slide text in on load
gsap.fromTo(contents[0]?.children ?? [],
  { y: 30, opacity: 0 },
  { y: 0, opacity: 1, duration: 0.6, stagger: 0.1, ease: 'power3.out', delay: 0.2 }
)

// Out: before the slide starts moving
slider.on('beforeSlideChange', ({ index }) => {
  const el = contents[index]
  if (!el) return
  gsap.to(el.children, {
    y: -20, opacity: 0, duration: 0.22, stagger: 0.05, ease: 'power2.in',
  })
})

// In: after the new slide is in position
slider.on('afterSlideChange', ({ index }) => {
  const el = contents[index]
  if (!el) return
  gsap.fromTo(el.children,
    { y: 36, opacity: 0 },
    { y: 0, opacity: 1, duration: 0.55, stagger: 0.09, ease: 'power3.out' }
  )
})

Staggered card entrance

A multi-slide layout where each card's inner elements animate in individually on afterSlideChange. The stagger creates a ripple feel — icon, title, description, and badge each appear 80ms apart. Touch or click an arrow to see the wave re-trigger.

Instant setup

One import, one constructor call. No config required.

Core
🎯

Events API

Fine-grained lifecycle hooks for any custom integration.

Lifecycle
🧩

Plugin system

Drop-in plugins — tree-shaken, typed, and composable.

Plugins
🌊

WebGL effects

GLSL shaders that speak the same event language.

WebGL

Accessible

ARIA roles, keyboard nav, and reduced-motion support built in.

A11y
View code
<div class="card-slider" id="my-cards">
  <div class="c--slider-a__wrapper">
    <div class="c--slider-a__slide card-slide" data-slide>
      <div class="card-body">
        <div class="card-icon">⚡</div>
        <h3 class="card-title">Instant setup</h3>
        <p class="card-text">One import, one constructor call. No config required.</p>
        <span class="card-badge">Core</span>
      </div>
    </div>
    <div class="c--slider-a__slide card-slide" data-slide>
      <div class="card-body">
        <div class="card-icon">🎯</div>
        <h3 class="card-title">Events API</h3>
        <p class="card-text">Fine-grained lifecycle hooks for any custom integration.</p>
        <span class="card-badge">Lifecycle</span>
      </div>
    </div>
    <div class="c--slider-a__slide card-slide" data-slide>
      <div class="card-body">
        <div class="card-icon">🧩</div>
        <h3 class="card-title">Plugin system</h3>
        <p class="card-text">Drop-in plugins — tree-shaken, typed, and composable.</p>
        <span class="card-badge">Plugins</span>
      </div>
    </div>
    <div class="c--slider-a__slide card-slide" data-slide>
      <div class="card-body">
        <div class="card-icon">🌊</div>
        <h3 class="card-title">WebGL effects</h3>
        <p class="card-text">GLSL shaders that speak the same event language.</p>
        <span class="card-badge">WebGL</span>
      </div>
    </div>
    <div class="c--slider-a__slide card-slide" data-slide>
      <div class="card-body">
        <div class="card-icon">♿</div>
        <h3 class="card-title">Accessible</h3>
        <p class="card-text">ARIA roles, keyboard nav, and reduced-motion support built in.</p>
        <span class="card-badge">A11y</span>
      </div>
    </div>
  </div>
</div>
<div id="my-cards-pag" class="c--slider-a__pagination"></div>
.card-slider {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: 8px;
  background: #f3f4f6;
}
.c--slider-a__wrapper { display: flex; will-change: transform; }

.card-slide {
  flex-shrink: 0;
  padding: 8px;
}
.card-body {
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: 28px 24px 24px;
  height: 220px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.card-icon  { font-size: 2rem; line-height: 1; }
.card-title { font-size: 1.1rem; font-weight: 700; color: #111827; margin: 0; }
.card-text  { font-size: 0.875rem; color: #6b7280; line-height: 1.5; flex: 1; margin: 0; }
.card-badge {
  display: inline-block;
  align-self: flex-start;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: #ede9fe;
  color: #6C2BD9;
  padding: 2px 10px;
  border-radius: 99px;
}

.c--slider-a__pagination { display: flex; justify-content: center; padding: 10px 0; gap: 6px; }
.c--slider-a__pagination-bullet { width: 8px; height: 8px; border-radius: 50%; background: #d1d5db; border: none; cursor: pointer; }
.c--slider-a__pagination-bullet--active { background: #6C2BD9; }

.c--slider-a__arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 10; width: 36px; height: 36px; border-radius: 50%;
  border: none; background: rgba(255,255,255,0.95);
  cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.15);
}
.c--slider-a__arrow--prev { left: 4px; }
.c--slider-a__arrow--next { right: 4px; }
.card-slider {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: 8px;
  background: #f3f4f6;
}
.c--slider-a__wrapper { display: flex; will-change: transform; }

.card-slide {
  flex-shrink: 0;
  padding: 8px;
}
.card-body {
  background: #fff;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  padding: 28px 24px 24px;
  height: 220px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.card-icon  { font-size: 2rem; line-height: 1; }
.card-title { font-size: 1.1rem; font-weight: 700; color: #111827; margin: 0; }
.card-text  { font-size: 0.875rem; color: #6b7280; line-height: 1.5; flex: 1; margin: 0; }
.card-badge {
  display: inline-block;
  align-self: flex-start;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  background: #ede9fe;
  color: #6C2BD9;
  padding: 2px 10px;
  border-radius: 99px;
}

.c--slider-a__pagination { display: flex; justify-content: center; padding: 10px 0; gap: 6px; }
.c--slider-a__pagination-bullet { width: 8px; height: 8px; border-radius: 50%; background: #d1d5db; border: none; cursor: pointer; }
.c--slider-a__pagination-bullet--active { background: #6C2BD9; }

.c--slider-a__arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 10; width: 36px; height: 36px; border-radius: 50%;
  border: none; background: rgba(255,255,255,0.95);
  cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.15);
}
.c--slider-a__arrow--prev { left: 4px; }
.c--slider-a__arrow--next { right: 4px; }
import { Slider } from '@andresclua/sliderkit'
import { arrows, pagination } from '@andresclua/sliderkit-plugins'
import { gsap } from 'gsap'

const slider = new Slider('#my-cards', {
  slidesPerPage: 3,
  gutter: 0,
  loop: true,
  speed: 400,
  plugins: [
    arrows(),
    pagination({ el: '#my-cards-pag', type: 'dots', clickable: true }),
  ],
})

const cards = [...slider.slides].map(s => s.querySelector('.card-body'))
const n = cards.length

function animateIn(index) {
  const card = cards[index % n]
  if (!card) return
  gsap.fromTo(card.children,
    { y: 24, opacity: 0 },
    { y: 0, opacity: 1, duration: 0.45, stagger: 0.08, ease: 'power3.out' }
  )
}

// Animate the initial visible cards
const spp = slider.getInfo().slidesPerPage
for (let i = 0; i < spp; i++) animateIn(i)

// Track direction so we know which side the new card enters from
let dir = 'next'
slider.on('beforeSlideChange', ({ direction }) => { dir = direction })

slider.on('afterSlideChange', ({ index, previousIndex }) => {
  const spp = slider.getInfo().slidesPerPage
  const entering = []

  if (dir === 'next') {
    // New card(s) sliding in from the right
    for (let i = Math.max(index, previousIndex + spp); i < index + spp; i++) {
      entering.push(i % n)
    }
  } else {
    // New card(s) sliding in from the left
    for (let i = index; i <= Math.min(index + spp - 1, previousIndex - 1); i++) {
      entering.push(i % n)
    }
  }

  // Loop boundary or large jump: animate all visible cards
  if (entering.length === 0) {
    for (let i = 0; i < spp; i++) entering.push((index + i) % n)
  }

  entering.forEach(animateIn)
})