Svelte Example: Sticky

svelte
<script lang="ts">
  import { faker } from '@faker-js/faker'
  import { findIndex, groupBy } from 'lodash'
  import {
    createVirtualizer,
    defaultRangeExtractor,
    type Range,
  } from '@tanstack/svelte-virtual'

  const groupedNames = groupBy(
    Array.from({ length: 1000 })
      .map(() => faker.person.firstName())
      .sort(),
    (name: any) => name[0],
  )
  const groups = Object.keys(groupedNames)
  const rows = groups.reduce((acc, k) => [...acc, k, ...groupedNames[k]], [])
  const stickyIndexes = groups.map((gn) =>
    findIndex(rows, (n: any) => n === gn),
  )

  let virtualListEl: HTMLDivElement
  let activeStickyIndex: number = 0

  $: virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
    count: rows.length,
    getScrollElement: () => virtualListEl,
    estimateSize: () => 50,
    overscan: 5,
  })

  $: {
    function rangeExtractor(range: Range): number[] {
      activeStickyIndex = [...stickyIndexes]
        .reverse()
        .find((index) => range.startIndex >= index)

      const next = new Set([activeStickyIndex, ...defaultRangeExtractor(range)])

      return [...next].sort((a, b) => a - b)
    }
    $virtualizer.setOptions({ rangeExtractor })
  }

  function isSticky(index: number) {
    return stickyIndexes.includes(index)
  }
  $: isActiveSticky = (index: number) => activeStickyIndex === index
</script>

<main>
  <div class="list scroll-container" bind:this={virtualListEl}>
    <div
      style="position: relative; height: {$virtualizer.getTotalSize()}px; width: 100%;"
    >
      {#each $virtualizer.getVirtualItems() as row (row.index)}
        <div
          class:sticky={isSticky(row.index)}
          class:active={isActiveSticky(row.index)}
          style={`top: 0; left: 0; width: 100%; height: ${row.size}px; ${
            !isActiveSticky(row.index)
              ? `position: absolute; transform: translateY(${row.start}px);`
              : ''
          }`}
        >
          {rows[row.index]}
        </div>
      {/each}
    </div>
  </div>
</main>

<style>
  .scroll-container {
    height: 200px;
    width: 400px;
    overflow: auto;
  }
  .sticky {
    position: absolute;
    background: #fff;
    border-bottom: 1px solid #ddd;
    z-index: 1;
  }
  .sticky.active {
    position: sticky;
  }
</style>
<script lang="ts">
  import { faker } from '@faker-js/faker'
  import { findIndex, groupBy } from 'lodash'
  import {
    createVirtualizer,
    defaultRangeExtractor,
    type Range,
  } from '@tanstack/svelte-virtual'

  const groupedNames = groupBy(
    Array.from({ length: 1000 })
      .map(() => faker.person.firstName())
      .sort(),
    (name: any) => name[0],
  )
  const groups = Object.keys(groupedNames)
  const rows = groups.reduce((acc, k) => [...acc, k, ...groupedNames[k]], [])
  const stickyIndexes = groups.map((gn) =>
    findIndex(rows, (n: any) => n === gn),
  )

  let virtualListEl: HTMLDivElement
  let activeStickyIndex: number = 0

  $: virtualizer = createVirtualizer<HTMLDivElement, HTMLDivElement>({
    count: rows.length,
    getScrollElement: () => virtualListEl,
    estimateSize: () => 50,
    overscan: 5,
  })

  $: {
    function rangeExtractor(range: Range): number[] {
      activeStickyIndex = [...stickyIndexes]
        .reverse()
        .find((index) => range.startIndex >= index)

      const next = new Set([activeStickyIndex, ...defaultRangeExtractor(range)])

      return [...next].sort((a, b) => a - b)
    }
    $virtualizer.setOptions({ rangeExtractor })
  }

  function isSticky(index: number) {
    return stickyIndexes.includes(index)
  }
  $: isActiveSticky = (index: number) => activeStickyIndex === index
</script>

<main>
  <div class="list scroll-container" bind:this={virtualListEl}>
    <div
      style="position: relative; height: {$virtualizer.getTotalSize()}px; width: 100%;"
    >
      {#each $virtualizer.getVirtualItems() as row (row.index)}
        <div
          class:sticky={isSticky(row.index)}
          class:active={isActiveSticky(row.index)}
          style={`top: 0; left: 0; width: 100%; height: ${row.size}px; ${
            !isActiveSticky(row.index)
              ? `position: absolute; transform: translateY(${row.start}px);`
              : ''
          }`}
        >
          {rows[row.index]}
        </div>
      {/each}
    </div>
  </div>
</main>

<style>
  .scroll-container {
    height: 200px;
    width: 400px;
    overflow: auto;
  }
  .sticky {
    position: absolute;
    background: #fff;
    border-bottom: 1px solid #ddd;
    z-index: 1;
  }
  .sticky.active {
    position: sticky;
  }
</style>
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.

Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.