Scroll fade mask

Use CSS mask-image with @property-registered custom properties to create smooth, animated fade edges on scrollable containers.

html #ui
<style>
  @property --fade-top-start {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 0%;
  }

  @property --fade-top-end {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 0%;
  }

  @property --fade-bottom-start {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 94%;
  }

  @property --fade-bottom-end {
    syntax: "<percentage>";
    inherits: false;
    initial-value: 99%;
  }

  * {
    box-sizing: border-box;
    margin: 0;
  }

  body {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  }

  .card {
    background: white;
    border-radius: 12px;
    border: 1px solid #e7e5e4;
    overflow: hidden;
    width: 100%;
    max-width: 360px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
  }

  .card-header {
    padding: 1rem 1.25rem 0.75rem;
    border-bottom: 1px solid #f0f0ee;
  }

  .card-title {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: #a8a29e;
  }

  .scroll-container {
    height: 360px;
    overflow-y: auto;
    scrollbar-width: none;

    --fade-top-start: 0%;
    --fade-top-end: 0%;
    --fade-bottom-start: 94%;
    --fade-bottom-end: 100%;

    mask-image: linear-gradient(
      to bottom,
      transparent var(--fade-top-start),
      #000 var(--fade-top-end),
      #000 var(--fade-bottom-start),
      transparent var(--fade-bottom-end)
    );
    mask-size: 100% 100%;
    mask-repeat: no-repeat;
    mask-position: center;

    transition:
      --fade-top-start 0.15s ease-out,
      --fade-top-end 0.15s ease-out,
      --fade-bottom-start 0.15s ease-out,
      --fade-bottom-end 0.15s ease-out;
  }

  .scroll-container::-webkit-scrollbar {
    display: none;
  }

  .scroll-container.scrolled-top {
    --fade-top-start: 0%;
    --fade-top-end: 6%;
  }

  .scroll-container.scrolled-bottom {
    --fade-bottom-start: 100%;
    --fade-bottom-end: 100%;
  }

  .list {
    padding: 0.5rem 0;
  }

  .list-item {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.625rem 1.25rem;
  }

  .avatar {
    width: 32px;
    height: 32px;
    border-radius: 50%;
    flex-shrink: 0;
  }

  .item-content {
    flex: 1;
    min-width: 0;
  }

  .item-name {
    font-size: 13px;
    font-weight: 500;
    color: #1c1917;
  }

  .item-detail {
    font-size: 12px;
    color: #a8a29e;
  }
</style>

<div class="card">
  <div class="card-header">
    <div class="card-title">Team members</div>
  </div>
  <div class="scroll-container" id="scroller">
    <div class="list" id="list"></div>
  </div>
</div>

<script>
  const PEOPLE = [
    "Olivia Martin",
    "Jackson Lee",
    "Sofia Davis",
    "Liam Johnson",
    "Emma Wilson",
    "Noah Brown",
    "Ava Garcia",
    "Ethan Martinez",
    "Isabella Rodriguez",
    "Mason Thompson",
    "Mia Anderson",
    "Lucas Taylor",
    "Charlotte Thomas",
    "Henry Jackson",
    "Amelia White",
    "Alexander Harris",
    "Harper Clark",
    "Sebastian Lewis",
    "Evelyn Robinson",
    "Daniel Walker",
    "Aria Young",
    "Matthew King",
    "Chloe Wright",
    "Owen Hill",
  ]

  const ROLES = [
    "Design",
    "Engineering",
    "Product",
    "Marketing",
    "Operations",
    "Research",
    "Support",
    "Analytics",
  ]

  const COLORS = [
    "#ef4444",
    "#f97316",
    "#eab308",
    "#22c55e",
    "#06b6d4",
    "#3b82f6",
    "#8b5cf6",
    "#ec4899",
  ]

  const list = document.getElementById("list")

  PEOPLE.forEach((name, i) => {
    const item = document.createElement("div")
    item.className = "list-item"

    const initials = name
      .split(" ")
      .map((n) => n[0])
      .join("")
    const color = COLORS[i % COLORS.length]
    const role = ROLES[i % ROLES.length]

    item.innerHTML = `
      <svg class="avatar" viewBox="0 0 32 32">
        <rect width="32" height="32" rx="16" fill="${color}20" />
        <text x="16" y="17" text-anchor="middle" dominant-baseline="central"
              font-size="11" font-weight="600" fill="${color}"
              font-family="-apple-system, BlinkMacSystemFont, sans-serif">${initials}</text>
      </svg>
      <div class="item-content">
        <div class="item-name">${name}</div>
        <div class="item-detail">${role}</div>
      </div>
    `

    list.appendChild(item)
  })

  const scroller = document.getElementById("scroller")

  function updateMask() {
    const { scrollTop, scrollHeight, clientHeight } = scroller
    const atTop = scrollTop <= 2
    const atBottom = scrollTop + clientHeight >= scrollHeight - 2

    scroller.classList.toggle("scrolled-top", !atTop)
    scroller.classList.toggle("scrolled-bottom", atBottom)
  }

  scroller.addEventListener("scroll", updateMask, { passive: true })
  updateMask()
</script>