npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@arcmantle/infinite-scroller

v1.0.0

Published

Infinite scroller base component.

Readme

@arcmantle/infinite-scroller

A high-performance infinite scrolling component that virtualizes large lists by only rendering visible items. Built with Lit and optimized for smooth scrolling with thousands of items.

Features

  • 🚀 Virtual Scrolling: Only renders items that are visible in the viewport
  • ⚡ High Performance: Handles thousands of items with minimal memory usage
  • 🔄 Dynamic Buffering: Automatically calculates optimal buffer sizes based on viewport
  • 📱 Responsive: Adapts to container size changes automatically
  • 🎯 Smooth Scrolling: Uses CSS transforms and optimized rendering for 60fps performance
  • 🛠️ Abstract Base Class: Extend and customize for your specific use cases
  • 🎨 Styleable: Full CSS customization with CSS custom properties

Installation

npm install @arcmantle/infinite-scroller

pnpm add @arcmantle/infinite-scroller

yarn add @arcmantle/infinite-scroller

Basic Usage

The InfiniteScroller is an abstract base class that you extend to create your own infinite scrolling components:

import { InfiniteScroller } from '@arcmantle/infinite-scroller';
import { customElement } from 'lit/decorators.js';

@customElement('my-list')
export class MyListComponent extends InfiniteScroller {

  constructor() {
    super();
    this.maxIndex = 10000; // Total number of items
  }

  // Create the DOM element for each list item
  protected createElement(): HTMLElement {
    return document.createElement('my-list-item');
  }

  // Update the element content based on its index
  protected updateElement(element: HTMLElement, index: number): void {
    if (index < 0 || index >= this.maxIndex) {
      element.style.visibility = 'hidden';
      return;
    }

    element.style.visibility = 'visible';
    element.textContent = `Item ${index}`;
  }
}

Advanced Example

Here's a more complete example with custom styling and data:

import { css, html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { InfiniteScroller } from '@arcmantle/infinite-scroller';

interface ListItem {
  id: string;
  title: string;
  description: string;
}

@customElement('data-list')
export class DataListComponent extends InfiniteScroller {

  @property({ type: Array })
  data: ListItem[] = [];

  constructor() {
    super();
    this.maxIndex = this.data.length;
  }

  protected createElement(): HTMLElement {
    return document.createElement('data-list-item');
  }

  protected updateElement(element: DataListItemComponent, index: number): void {
    if (index < 0 || index >= this.data.length) {
      element.style.visibility = 'hidden';
      return;
    }

    element.style.visibility = 'visible';
    element.item = this.data[index];
  }

  // Handle infinite loading
  protected override onScroll(): void {
    super.onScroll();

    // Load more data when near the end
    if ((this.maxIndex - this.position) < 30) {
      this.loadMoreData();
    }
  }

  private async loadMoreData(): Promise<void> {
    const newData = await fetchMoreItems();
    this.data = [...this.data, ...newData];
    this.maxIndex = this.data.length;
  }

  static override styles = css`
    :host {
      --item-height: 80px;
      height: 400px;
      border: 1px solid #ccc;
    }
  `;
}

@customElement('data-list-item')
export class DataListItemComponent extends LitElement {

  @property({ type: Object })
  item?: ListItem;

  protected render() {
    if (!this.item) return html``;

    return html`
      <div class="item">
        <h3>${this.item.title}</h3>
        <p>${this.item.description}</p>
      </div>
    `;
  }

  static styles = css`
    :host {
      display: block;
      padding: 16px;
      border-bottom: 1px solid #eee;
    }

    .item h3 {
      margin: 0 0 8px 0;
      font-size: 16px;
    }

    .item p {
      margin: 0;
      color: #666;
      font-size: 14px;
    }
  `;
}

Properties

Core Properties

| Property | Type | Description | |----------|------|-------------| | maxIndex | number | Total number of items in the list | | itemHeight | number | Height of each item in pixels | | bufferSize | number | Number of items to render outside viewport (calculated automatically) | | position | number | Current scroll position as item index (can be fractional) |

CSS Custom Properties

| Property | Default | Description | |----------|---------|-------------| | --item-height | 60px | Height of each list item |

Methods

Abstract Methods (Must Implement)

// Create a new DOM element for list items
protected abstract createElement(): HTMLElement;

// Update element content based on its index position
protected abstract updateElement(element: HTMLElement, index: number): void;

Public Methods

// Get/set scroll position by item index
scroller.position = 100; // Scroll to item 100
const currentPosition = scroller.position;

Lifecycle Hooks

// Override to handle scroll events
protected onScroll(): void {
  super.onScroll();
  // Your custom scroll logic
}

// Override to handle resize events
protected onResize(entries?: ResizeObserverEntry[]): boolean {
  const result = super.onResize(entries);
  // Your custom resize logic
  return result;
}

Architecture

Virtual Scrolling Strategy

The infinite scroller uses a dual-buffer strategy:

  1. Two Buffers: Maintains two buffer zones that contain rendered items
  2. Dynamic Translation: Buffers are translated vertically as the user scrolls
  3. Automatic Sizing: Buffer size is calculated based on viewport height
  4. Smart Updates: Only updates items that are actually visible

Buffer Management

┌─────────────────┐
│   Buffer 0      │ ← Contains items 0-19
├─────────────────┤
│   Buffer 1      │ ← Contains items 20-39
├─────────────────┤
│   (Virtual)     │ ← Items 40+ not rendered
└─────────────────┘

As the user scrolls down:

  • Buffer 0 moves below Buffer 1 and updates to show items 40-59
  • Buffer 1 continues showing items 20-39
  • The cycle continues seamlessly

Performance Optimizations

  • CSS Transforms: Uses translate3d() for hardware acceleration
  • Passive Scrolling: Scroll listeners are passive for better performance
  • ResizeObserver: Efficiently handles container size changes
  • Minimal DOM: Only creates elements that fit in the buffers
  • Smart Updates: Only updates visible elements during scroll

Events

Custom Events

// Fired when the scroller is ready and initialized
scroller.addEventListener('ready', (event) => {
  console.log('Scroller is ready');
});

Native Events

The component supports all standard scroll events on the internal scroller element.

Styling

Basic Styling

my-list {
  --item-height: 100px;
  height: 500px;
  width: 100%;
  border: 1px solid #ddd;
}

Advanced Styling with CSS Parts

my-list::part(scroller) {
  border-radius: 8px;
  background: #f9f9f9;
}

my-list::part(buffer) {
  /* Style the buffer containers */
}

Responsive Design

my-list {
  --item-height: 60px;
  height: 100%;
}

@media (max-width: 768px) {
  my-list {
    --item-height: 80px;
  }
}

Common Patterns

Infinite Loading

protected override onScroll(): void {
  super.onScroll();

  const threshold = 50; // Items from end
  if ((this.maxIndex - this.position) < threshold) {
    this.loadMoreItems();
  }
}

private async loadMoreItems(): Promise<void> {
  if (this.loading) return;

  this.loading = true;
  const newItems = await this.dataService.fetchMore();
  this.data.push(...newItems);
  this.maxIndex = this.data.length;
  this.loading = false;
}

Search and Filter

private filteredData: Item[] = [];

search(query: string): void {
  this.filteredData = this.allData.filter(item =>
    item.title.toLowerCase().includes(query.toLowerCase())
  );
  this.maxIndex = this.filteredData.length;
  this.position = 0; // Reset to top
}

protected updateElement(element: HTMLElement, index: number): void {
  const item = this.filteredData[index];
  // Update element with filtered data
}

Dynamic Item Heights

For variable item heights, calculate and cache heights:

private itemHeights = new Map<number, number>();

protected override get itemHeight(): number {
  // Return average height or base height
  return this.averageItemHeight || 60;
}

protected updateElement(element: HTMLElement, index: number): void {
  super.updateElement(element, index);

  // Measure and cache actual height
  requestAnimationFrame(() => {
    const height = element.getBoundingClientRect().height;
    this.itemHeights.set(index, height);
  });
}

Browser Support

  • Modern Browsers: Chrome 69+, Firefox 63+, Safari 12+
  • Required Features:
    • ResizeObserver API
    • CSS Grid Layout
    • CSS Custom Properties
    • ES2020+ JavaScript features

Performance Tips

  1. Keep Updates Light: Minimize work in updateElement()
  2. Use CSS for Styling: Prefer CSS over JavaScript for visual changes
  3. Batch DOM Updates: Group multiple changes together
  4. Profile Your Code: Use browser dev tools to identify bottlenecks
  5. Consider Item Complexity: Simpler items = better performance

Troubleshooting

Common Issues

Items not updating correctly:

  • Ensure updateElement() handles all edge cases
  • Check that maxIndex is set correctly

Scrolling feels sluggish:

  • Reduce complexity in updateElement()
  • Check for memory leaks in item components

Layout jumping:

  • Ensure consistent --item-height values
  • Avoid dynamic height changes during scroll

Buffer size warnings:

  • Increase container height or decrease item height
  • Check for CSS issues affecting measurements

Development

Building

pnpm install
pnpm build

Development Server

pnpm dev

Testing

pnpm test

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

This project is licensed under the Apache 2.0 License - see the Apache License 2.0 for details.

Related Packages

This component is part of the @arcmantle ecosystem:

  • @arcmantle/library - Core utilities and helper functions
  • lit - The underlying web component framework

.