<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<div class="pen">
  <div class="notifications">
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
    <div class="notification"></div>
  </div>
</div>
<a class="created-by" target="_blank" href="https://popmotion.io">
  Pen created with
  <svg class="logo" width="125" height="25" viewBox="0 0 200 41">
    <defs>
      <linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="popmotion-gradient">
        <stop stop-color="#FF1C68" offset="0%"/>
        <stop stop-color="#FF1C68" offset="100%"/>
      </linearGradient>
    </defs>
    <path
          fill="url(#popmotion-gradient)"
          d="M15.65.85c6.3 0 9.6 4.17 9.6 9.4 0 5.93-4.3 11.33-11.78 11.33H8.8l-1.04 10.2H.28L3.48.85h12.17zm-5.4 7.03l-.77 7.03h4.26c2.54 0 4.04-1.67 4.04-3.9 0-1.58-1-3.12-3.13-3.12h-4.4zM23.36 21.85c0-7 5.85-12.2 12.8-12.2 5.98 0 10.65 4.4 10.65 10.43 0 7.03-6.02 12.15-12.78 12.15-6.12 0-10.66-4.35-10.66-10.38zm16.28-1.27c0-2.4-1.58-4.4-4.03-4.4-2.85 0-5.03 2.4-5.03 5.2 0 2.33 1.55 4.32 4 4.32 2.9 0 5.07-2.4 5.07-5.12zM55.66 10.1l.1 2.3s1.86-2.75 6.12-2.75c4.94 0 8.8 4.17 8.8 10.25 0 7.03-5.36 12.33-11.02 12.33-3.9 0-5.5-2.35-5.5-2.35l-1.13 10.97h-7.2l3.2-30.75h6.63zm8 10.3c0-2.32-1.55-4.22-4.14-4.22-2.77 0-4.13 2.18-4.13 2.18l-.55 5.12s.9 2.18 3.67 2.18c2.95 0 5.13-2.45 5.13-5.26zM91.18 31.78h-7.2l1.35-12.8c0-.13.05-.4.05-.67 0-1.26-.64-1.98-1.95-1.98-1.68 0-3.54 1.67-3.54 1.67l-1.46 13.78h-7.2L73.5 10.1h6.8l.1 2.18s2.98-2.54 6.3-2.54c3.53 0 4.66 2.63 4.66 2.63s3.27-2.63 6.94-2.63c4.86 0 7.08 2.72 7.08 7.12 0 .54-.05 1.13-.1 1.68l-1.35 13.24H96.7l1.33-12.38c.05-.45.05-.64.05-.9 0-1.5-.77-2.18-1.95-2.18-1.77 0-3.5 1.72-3.5 1.72l-1.45 13.74M106.16 21.85c0-7 5.85-12.2 12.8-12.2 5.98 0 10.65 4.4 10.65 10.43 0 7.03-6.02 12.15-12.78 12.15-6.13 0-10.66-4.35-10.66-10.38zm16.28-1.27c0-2.4-1.6-4.4-4.03-4.4-2.85 0-5.03 2.4-5.03 5.2 0 2.33 1.54 4.32 4 4.32 2.9 0 5.07-2.4 5.07-5.12zM140.4 10.1l.5-4.76h-7.2l-.5 4.76h-3.13l-.68 6.53h3.12l-1.6 15.15h7.23l1.58-15.15h5.18l-1.57 15.15h7.26l2.25-21.68H140.4M146.46.9l-.64 6.16h7.5l.62-6.16h-7.48M153.05 21.85c0-7 5.85-12.2 12.8-12.2 5.97 0 10.65 4.4 10.65 10.43 0 7.03-6.04 12.15-12.8 12.15-6.12 0-10.65-4.35-10.65-10.38zm16.28-1.27c0-2.4-1.6-4.4-4.04-4.4-2.86 0-5.04 2.4-5.04 5.2 0 2.33 1.54 4.32 4 4.32 2.9 0 5.07-2.4 5.07-5.12zM198 31.78h-7.2l1.26-12.02c.05-.4.05-.63.05-.95 0-1.5-.76-2.62-2.62-2.62-2.4 0-4.36 1.95-4.36 1.95l-1.4 13.65h-7.2l2.25-21.68h6.9v2.3s2.9-2.7 6.44-2.7c3.95 0 7.3 2.53 7.3 7.75 0 .4-.04 1-.08 1.5L198 31.77"
          />
  </svg>
</a>
body {
  --pink: #FF1C68;
  --green: #14D790;
  --blue: #198FE3;
  --white: #fff;
  background: var(--white);
  font-family: 'Source Sans Pro', sans-serif;
  height: 100vh;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
}

.pen {
  flex: 1 1 100%;
}

.created-by {
  flex: 0 0 50px;
  background: #fff;
  color: #222;
  text-decoration: none;
  display: flex;
  justify-content: flex-end;
  align-items: center;
  padding-right: 30px;
}

.logo {
  margin-left: 10px;
}

.pen {
  display: flex;
  justify-content: center;
  align-items: center;
}

.notifications {
  width: 300px;
  height: 350px;
  position: relative;
}

.notification {
  height: 40px;
  width: 100%;
  border-radius: 5px;
  position: absolute;
  top: 0;
  
  $numNotifications: 15;
  $step: 360 / $numNotifications;
  @for $i from 1 through $numNotifications {
    &:nth-child(#{$i}) {
      background: hsla($i * $step, 100%, 60%, 1);
      border: 1px solid hsla($i * $step, 100%, 40%, 1);
      z-index: $numNotifications - $i + 1;
    }
  }
  
  &:last-child {
    margin-bottom: 0;
  }
}
View Compiled
// Import Popmotion
const { decay, listen, styler, value, spring, transform, pointer } = window.popmotion;
const { clamp, interpolate, conditional, nonlinearSpring, pipe } = transform;

// Define helper functions
const height = (element) => element.getBoundingClientRect().height;
const addItemHeight = (gap) => (total, item) => total + height(item) + gap;
const pointerY = y => pointer({ y }).pipe(v => v.y);

// Select items
const container = document.querySelector('.notifications');
const items = Array.from(document.querySelectorAll('.notification'));

// Set item gap sizes
const gapCollapsed = 5;
const gapSpread = 10;

// Measure dimensions of list
const containerHeight = height(container);
const containerScrollHeight = items.reduce(addItemHeight(gapSpread), 0) - gapSpread;
const maxScroll = containerScrollHeight - containerHeight;

// Create subscribable reactive stream from scroll position
const scrollPosition = value(0);

// Calculate mapping of scrollPosition to itemPosition and subscribe each
// item to scrollPosition
const numItems = items.length;
items.forEach((item, i) => {
  // Calculate min and max y offsets for item
  const { offsetHeight } = item;
  const offsetTop = i * (offsetHeight + gapSpread);
  const minY = i * gapCollapsed;
  const maxY = containerHeight - offsetHeight - ((numItems - i - 1) * gapCollapsed);

  // Calculate when to start moving item from bottom stack
  const scrollStart = offsetTop - maxY;
  const scrollEnd = offsetTop - minY;
  
  // Define mapping from scroll position to item y
  const inputRange = [scrollStart, scrollEnd];
  const outputRange = [maxY, minY];
  
  // Create streams and subscribe to scrollPosition
  const itemStyler = styler(item);
  const yValue = value(scrollPosition.get())
    .pipe(clamp(...inputRange), interpolate(inputRange, outputRange));

  yValue.subscribe(itemStyler.set('y'));
  scrollPosition.subscribe(yValue);
  
  // Reverse z-index
  itemStyler.set('z-index', numItems - i);
});

// Add triggering events
listen(container, 'wheel')
  .pipe(
    ({ deltaY }) => scrollPosition.get() + deltaY,
    clamp(0, maxScroll)
  )
  .start(v => scrollPosition.update(v));

listen(container, 'touchstart mousedown')
  .start((e) => {
    e.preventDefault();
    pointerY(- scrollPosition.get())
      .pipe(
        v => -v,
        conditional(v => v < 0, nonlinearSpring(2, 0)),
        conditional(v => v > maxScroll, nonlinearSpring(2, maxScroll))
      )
      .start(scrollPosition);
  });

const snapToEnd = y => spring({
  from: y,
  to: y < 0 ? 0 : maxScroll,
  velocity: scrollPosition.getVelocity() * 0.75,
  stiffness: 500,
  damping: 20
}).start(scrollPosition);

listen(document, 'touchend mouseup')
  .start(() => {
    const y = scrollPosition.get();
  
    // If out of range, snap to nearest limit
    if (y < 0 || y > maxScroll) {
      snapToEnd(y);
      
    // Or momentum scroll
    } else {
      decay({
        from: scrollPosition.get(),
        velocity: scrollPosition.getVelocity()
      }).pipe(v => {
          if (v < 0 || v > maxScroll) snapToEnd(v);
          return v;
        })
        .start(scrollPosition);
    }
  });
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.com/popmotion/dist/popmotion.global.min.js