# How to Implement Infinite Scrolling for the Exercise Browser

> Learn how to implement infinite scrolling for your exercise browser. Use the Intersection Observer API and slice your JSON data for a seamless user experience without page reloads.

- Repository: [Hasan Emir Yıldırım/exercises-dataset](https://github.com/hasaneyldrm/exercises-dataset)
- Tags: how-to-guide
- Published: 2026-07-17

---

**Add a sentinel element to [`index.html`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/index.html), use the Intersection Observer API to detect when users scroll near the bottom, and slice the static [`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json) file into pages to append new cards without reloading the entire list.**

The `hasaneyldrm/exercises-dataset` repository provides a static exercise browser that renders exercises from a local JSON file. To implement infinite scrolling for the exercise browser while maintaining the static-site architecture, you will paginate the dataset client-side using a sentinel-based detection system.

## Architecture Overview

The implementation requires three core components working together. First, **[`index.html`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/index.html)** provides the container (`#exercise-list`) and a hidden sentinel element that marks the scroll trigger. Second, **[`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json)** serves as the static data source, which the client fetches once and caches in memory. Third, **[`scripts/infiniteScroll.js`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/scripts/infiniteScroll.js)** (a new file you will create) handles pagination logic, scroll detection, and DOM manipulation.

This approach avoids server-side changes by treating the static JSON file as a paginated API, slicing the array in memory as the user scrolls.

## Setting Up the HTML Sentinel

The infinite scroll mechanism relies on a **sentinel element** placed immediately after the exercise list. When this sentinel enters the viewport, it triggers the next data load.

Update your [`index.html`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/index.html) to include the container and sentinel:

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

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

```

The `#sentinel` div remains empty and hidden (or minimally styled) while the `#exercise-list` container holds all rendered exercise cards.

## Fetching and Caching the Dataset

Because the repository is client-side only, the "server" is simulated by loading the entire [`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json) file once, then serving slices from an in-memory array.

Create [`scripts/infiniteScroll.js`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/scripts/infiniteScroll.js) and initialize the pagination state:

```javascript
// 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

// Load the dataset once on startup
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));

```

Caching the full dataset in `allExercises` ensures that subsequent page loads are instantaneous, as no additional network requests are required after the initial fetch.

## Detecting the Scroll Position

Instead of attaching expensive `scroll` event listeners to the window, use the **Intersection Observer API** to detect when the sentinel element becomes visible. This approach is more performant and works regardless of scroll container context.

Implement the observer function:

```javascript
function observeSentinel() {
  const sentinel = document.getElementById('sentinel');
  const observer = new IntersectionObserver(entries => {
    if (entries[0].isIntersecting) {
      loadNextPage();
    }
  });
  observer.observe(sentinel);
}

```

The observer watches the `#sentinel` div and fires `loadNextPage()` whenever the element enters the viewport, indicating the user has scrolled to the bottom of the current list.

## Rendering and Appending Items

When triggered, the pagination logic slices the next chunk of exercises from the cached array and appends them to the DOM without re-rendering existing content.

Add the rendering functions to [`scripts/infiniteScroll.js`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/scripts/infiniteScroll.js):

```javascript
// 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;
}

```

The **`loadNextPage()`** function guards against over-fetching by checking `offset` against `allExercises.length`, automatically stopping when the end of the dataset is reached.

## Optional Enhancements

You can extend the basic implementation with several UX improvements:

- **Loading indicators**: Add a spinner inside the sentinel element that displays while `loadNextPage()` processes, removing it once new cards are appended.
- **Persistence**: Store the current `offset` in `localStorage` during the `loadNextPage()` execution, then check for this value on initial load to restore the user's scroll position after a refresh.
- **Debouncing**: Wrap the Intersection Observer callback with a debounce mechanism if you increase `PAGE_SIZE` significantly, preventing rapid consecutive renders when the sentinel remains in view during heavy DOM manipulation.

## Summary

- **Use the Intersection Observer API** instead of `scroll` event listeners for better performance and cleaner code.
- **Place a sentinel element** (`#sentinel`) after the last card in [`index.html`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/index.html) to act as the scroll trigger.
- **Slice the static [`data/exercises.json`](https://github.com/hasaneyldrm/exercises-dataset/blob/main/data/exercises.json)** into managable pages (default 20 items) using `Array.prototype.slice()` to simulate API pagination.
- **Append new cards** to `#exercise-list` incrementally without re-rendering the entire list, preserving scroll position and minimizing DOM reflow.

## Frequently Asked Questions

### Why should I use Intersection Observer instead of scroll event listeners?

The **Intersection Observer API** offloads scroll position calculations to the browser engine, providing better performance than manually checking `window.scrollY` or `element.scrollTop` on every frame. It also handles edge cases like scrollable containers that are not the main viewport, and automatically manages threshold detection for when the sentinel becomes visible.

### How do I handle the end of the dataset?

The `loadNextPage()` function includes a guard clause (`if (offset >= allExercises.length) return;`) that stops execution when the offset exceeds the total array length. You can extend this by hiding the sentinel element or displaying a "No more exercises" message when the final slice is rendered.

### Can I adapt this code for a real backend API?

Yes. Replace the `fetch(EXERCISES_URL)` logic with a paginated endpoint (e.g., `/api/exercises?offset=${offset}&limit=${PAGE_SIZE}`) and modify `loadNextPage()` to fetch only the next slice from the server instead of slicing the local `allExercises` array. Keep the Intersection Observer implementation identical.

### How do I prevent duplicate requests while loading?

If using a real API, add a boolean `isLoading` flag that sets to `true` at the start of `loadNextPage()` and resets to `false` in a `.finally()` block. Check this flag at the beginning of the function and return early if a request is already in progress. With the static JSON approach shown above, requests are synchronous after the initial fetch, so duplicates are not a concern.