How to Implement Infinite Scroll in the Exercise Browser (index.html)

Infinite scroll in the hasaneyldrm/exercises-dataset exercise browser is achieved by adding a sentinel element to index.html, caching data/exercises.json client-side, and using the Intersection Observer API in scripts/infiniteScroll.js to append slices of 20 exercises at a time to the #exercise-list container.

The exercise browser in the hasaneyldrm/exercises-dataset repository is a static HTML page that currently renders its full catalog from a single JSON file. Implementing infinite scroll in the exercise browser replaces that single-page dump with a chunked, performance-friendly experience that loads data as the user scrolls.

Architecture for a Static Infinite Scroll

Because the repository is pure client-side, the infinite-scroll behavior is simulated by loading data/exercises.json once into memory and slicing it on the client. This keeps the static-site nature of the repository intact while providing a fluid, paginated user experience. The implementation relies on three core pieces: the container and sentinel in index.html, the master data in data/exercises.json, and the pagination engine in scripts/infiniteScroll.js.

Marking the Scroll Trigger in index.html

The main page requires a container to hold exercise cards and a hidden sentinel element that signals when to load more. In index.html, add the following markup:

<body>
  <main id="exercise-list"></main>
  <!-- Sentinel that will be observed -->
  <div id="sentinel"></div>

  <script src="scripts/infiniteScroll.js"></script>
</body>

Implementing the Pagination Engine

Create scripts/infiniteScroll.js to cache the dataset, render slices, and detect scroll position. The script reads ../data/exercises.json once, stores it in the allExercises array, and appends pages of 20 items to the existing DOM without re-rendering the full list.

// Path to the static JSON file
const EXERCISES_URL = '../data/exercises.json';
const PAGE_SIZE = 20;          // Number of exercises per request
let allExercises = [];         // Holds the full dataset after first load
let offset = 0;                // Current cursor

// Render a slice of exercises into the DOM
function renderExercises(slice) {
  const container = document.getElementById('exercise-list');
  slice.forEach(ex => {
    const card = document.createElement('article');
    card.className = 'exercise-card';
    card.innerHTML = `
      <h2>${ex.title}</h2>
      <p>${ex.description}</p>`;
    container.appendChild(card);
  });
}

// Load the next page and render it
function loadNextPage() {
  if (offset >= allExercises.length) return; // No more items

  const nextSlice = allExercises.slice(offset, offset + PAGE_SIZE);
  renderExercises(nextSlice);
  offset += PAGE_SIZE;
}

// Initial fetch of the whole data file (only once)
fetch(EXERCISES_URL)
  .then(res => res.json())
  .then(data => {
    allExercises = data;
    loadNextPage();        // Render the first page
    observeSentinel();     // Start listening for scroll events
  })
  .catch(err => console.error('Failed to load exercises:', err));

// Set up IntersectionObserver on the sentinel element
function observeSentinel() {
  const sentinel = document.getElementById('sentinel');
  const observer = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting) {
      loadNextPage();
    }
  });
  observer.observe(sentinel);
}

The sentinel stays at the bottom of the document. When it enters the viewport, entries[0].isIntersecting becomes true and loadNextPage() appends the next batch of exercises. Because the data is cached in allExercises after the first fetch, subsequent calls are instantaneous.

Handling the End of the Dataset

When the offset cursor reaches allExercises.length, the loadNextPage() function returns early and stops requesting new slices. This prevents unnecessary renders and gracefully handles the end of the static catalog. If the repository ever gains a real back-end, replace the in-memory slice() logic with paginated API requests such as /exercises?offset=…&limit=….

Optional Enhancements

  • Add a loading spinner that displays while the next page is being prepared.
  • Persist offset in localStorage so users resume their scroll position after a page refresh.
  • Debounce the observer callback if the page size is large, preventing rapid consecutive renders when the sentinel flickers.

Summary

  • Implementing infinite scroll in the exercise browser requires adding a sentinel element to index.html, caching data/exercises.json client-side, and slicing the array in scripts/infiniteScroll.js.
  • The Intersection Observer API triggers loadNextPage() when the sentinel enters the viewport, which is more performant than traditional scroll event listeners.
  • The renderExercises() function appends new cards to #exercise-list without re-rendering existing items, minimizing DOM thrashing.
  • Once offset exceeds allExercises.length, pagination stops automatically, providing a clean end-of-data experience.

Frequently Asked Questions

How do I stop infinite scroll when all exercises are loaded?

The loadNextPage() function checks if (offset >= allExercises.length) return; before slicing the array. When the offset exceeds the total number of exercises, the function exits silently and no additional cards are appended.

Why is Intersection Observer better than a scroll event listener?

The Intersection Observer API delegates visibility detection to the browser, eliminating the need to poll window.scrollY on every frame. It is more performant and works correctly even when the scrollable container is an element other than the window.

Can infinite scroll work without loading the entire JSON file first?

In the current hasaneyldrm/exercises-dataset architecture, the simplest approach is to fetch data/exercises.json once and slice it on the client. If a back-end is introduced later, replace the single fetch with paginated API requests using query parameters like ?offset=${offset}&limit=20.

What is the role of the sentinel element?

The sentinel is an empty <div id="sentinel"></div> placed after the last rendered card in index.html. The IntersectionObserver watches this element, and when it scrolls into view, it triggers the next page load.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →