ul.visual-rolodex
View Compiled
@import url('https://fonts.googleapis.com/css2?family=MuseoModerno:wght@100;200;300;400;500;600&display=swap')

:root
  --font-size 2

*
  box-sizing border-box
  
body
  min-height 100vh
  display flex
  align-items center
  justify-content center
  background hsl(0, 0%, 10%)
  perspective 800px
  transform-style preserve-3d

.visual-rolodex
  padding calc(50vh - 50px) 0
  font-size calc(var(--font-size) * 1rem)
  list-style none
  color hsl(0, 0%, 99%)
  font-family 'MuseoModerno', cursive
  transform rotateY(0.5deg)
  
  li
    display flex
    position relative
    align-items center
    font-variation-settings 'wght' var(--font-weight, 300)

  img
    height 100px
    width 100px
    object-fit cover
    margin-right 1rem
    filter grayscale(var(--grayscale))
    border-radius 50%
    
    
View Compiled
const {
  faker: {
    image: { avatar },
    name: { findName }
  },
  gsap: {
    to,
    set,
    registerPlugin,
    timeline,
    utils: { mapRange }
  },
  ScrollTrigger
} = window;

const ROLODEX = document.querySelector(".visual-rolodex");

const PEOPLE = [];

const genPerson = () => PEOPLE.push(findName());

for (let p = 0; p < 200; p++) genPerson();

for (const person of PEOPLE.sort()) {
  const ITEM = document.createElement("li");
  const NAME = document.createElement('span');
  const AVATAR = document.createElement('img');
  AVATAR.src = avatar()
  AVATAR.setAttribute('alt', person)
  NAME.innerHTML = person;
  ITEM.appendChild(AVATAR)
  ITEM.appendChild(NAME)
  ROLODEX.appendChild(ITEM);
}

const ITEMS = [...document.querySelectorAll('li')]

set(ITEMS, { scale: 0.35, opacity: 0.4, z: 250, '--font-weight': 300, '--grayscale': 1 })
set(ITEMS[0], { scale: 1, opacity: 1, z: 0, '--font-weight': 600, '--grayscale': 0})

let CURRENT = 0

ScrollTrigger.create({
  trigger: "body",
  start: "top top",
  end: "bottom bottom",
  onUpdate: self => {
    const INDEX = mapRange(0, 1, 0, PEOPLE.length - 1, self.progress)
    if (CURRENT !== Math.floor(INDEX)) {
      timeline({
        onComplete: () => {
          set(ITEMS.filter((item, idx) => idx !== CURRENT && idx !== INDEX), { opacity: 0.4, z: 250, scale: 0.35, '--font-weight': 300, '--grayscale': 1 })
          CURRENT = Math.floor(INDEX)
        }
      })
        .to(ITEMS[CURRENT], { scale: 0.35, duration: 0.1, opacity: 0.4, z: 250, '--font-weight': 300, '--grayscale': 1}, 0)
        .to(ITEMS[Math.floor(INDEX)], { scale: 1, duration: 0.1, opacity: 1, z: 0, '--font-weight': 600, '--grayscale': 0}, 0)
    }

  }
});
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js
  2. https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/ScrollTrigger.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.min.js