.gallery.hidden.fixed.w-56.h-72.transform(class="top-1/2 left-1/2 -translate-y-2/4 -translate-x-2/4")
  .gallery__container
    ul.gallery__content
      - let c = 0
      while c < 50
        li.gallery__card.bg-blue-200.w-56.h-72.rounded-md.absolute
          .card__content
            .card__title= c
        - c++
  .gallery__actions.absolute.top-full.flex(class="left-1/2")
    button.gallery__next.bg-red-200.p-4.rounded-md Next
    button.gallery__prev.bg-red-200.p-4.rounded.md Prev
View Compiled
*
  box-sizing border-box

body
  background hsl(220, 34%, 24%)
  min-height 100vh
  width calc(50 * 14rem)
  padding 0
  margin 0
View Compiled
import gsap from 'https://cdn.skypack.dev/gsap'
import ScrollTrigger from 'https://cdn.skypack.dev/gsap/ScrollTrigger'

const {
  to,
  set,
  registerPlugin,
  timeline,
  utils: { mapRange },
} = gsap
registerPlugin(ScrollTrigger)

const CARDS = [...document.querySelectorAll('.gallery__card')]
// const GALLERY = document.querySelector('.gallery')
// const CONTENT = document.querySelector('.gallery__content')
const NEXT = document.querySelector('.gallery__next')
const PREV = document.querySelector('.gallery__prev')

let INDEX = 0

set(CARDS, {
  opacity: 0,
  scale: 0,
})

const RENDER = scroller => {
  const NEW_INDEX = Math.floor(
    mapRange(0, 1, 0, CARDS.length, scroller.progress)
  )

  INDEX = NEW_INDEX
  const L1 = INDEX - 1 < 0 ? null : INDEX - 1
  const L2 = INDEX - 2 < 0 ? null : INDEX - 2
  const L3 = INDEX - 3 < 0 ? null : INDEX - 3
  const U1 = INDEX + 1 > CARDS.length - 1 ? null : INDEX + 1
  const U2 = INDEX + 2 > CARDS.length - 1 ? null : INDEX + 2
  const U3 = INDEX + 3 > CARDS.length - 1 ? null : INDEX + 3

  // console.info(INDEX, L3, L2, L1, U3, U2, U1)

  const CHANGE = timeline().set(
    [
      ...CARDS.filter(
        (c, i) =>
          i !== INDEX &&
          i !== L1 &&
          i !== L2 &&
          i !== L3 &&
          i !== U1 &&
          i !== U2 &&
          i !== U3
      ),
    ],
    {
      xPercent: idx => (idx < INDEX ? -400 : 400),
      opacity: 0,
    }
  )
  if (L3 !== null)
    CHANGE.to(
      CARDS[L3],
      {
        duration: 0.1,
        opacity: 0,
        xPercent: -300,
        scale: 0,
      },
      0
    )
  if (U3 !== null)
    CHANGE.to(
      CARDS[U3],
      {
        duration: 0.1,
        opacity: 0,
        xPercent: 300,
        scale: 0,
      },
      0
    )
  if (L2 !== null)
    CHANGE.to(
      CARDS[L2],
      {
        duration: 0.1,
        opacity: 0.25,
        scale: 0.25,
        xPercent: -100,
      },
      0
    )
  if (L1 !== null)
    CHANGE.to(
      CARDS[L1],
      {
        duration: 0.1,
        opacity: 0.5,
        scale: 0.5,
        xPercent: -50,
      },
      0
    )
  if (U2 !== null)
    CHANGE.to(
      CARDS[U2],
      {
        duration: 0.1,
        opacity: 0.25,
        scale: 0.25,
        xPercent: 100,
      },
      0
    )
  if (U1 !== null)
    CHANGE.to(
      CARDS[U1],
      {
        duration: 0.1,
        opacity: 0.5,
        scale: 0.5,
        xPercent: 50,
      },
      0
    )

  CHANGE.to(
    CARDS[INDEX],
    {
      duration: 0.1,
      opacity: 1,
      scale: 1,
      xPercent: 0,
    },
    0
  )
}
const SCROLLER = ScrollTrigger.create({
  horizontal: true,
  scroller: document.body,
  start: 'left left',
  end: 'right right',
  snap: 1 / 50,
  markers: true,
  onUpdate: self => {
    const NEW_INDEX = Math.floor(mapRange(0, 1, 0, CARDS.length, self.progress))
    if (CARDS[NEW_INDEX] && NEW_INDEX !== INDEX) RENDER(self)
  },
})
RENDER(SCROLLER)

NEXT.addEventListener('click', () => {
  SCROLLER.scroll((INDEX + 1) * (SCROLLER.end / (CARDS.length - 1)))
})
PREV.addEventListener('click', () => {
  SCROLLER.scroll((INDEX - 1) * (SCROLLER.end / (CARDS.length - 1)))
})

set('.gallery', { display: 'block' })
set(document.body, { scrollLeft: 0 })
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.0.2/tailwind.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.