Blog

Loading More with Less: Infinite Scrolling in Go and HTMX

On Friday, Mar 14, 2025
post image

Handling large datasets in web applications can be challenging. The data is typically paginated via the antiquated method of splitting the data into small chunks, but this takes user action and interrupts browse flow. Infinite scrolling, however, loads asynchronously as the user scrolls, and browsing becomes an engaging experience.

Modern applications are designed to keep users glued to them for as long as possible. Social media, news feeds, and shopping websites all rely on seamless content consumption in order to realize maximum retention. Infinite scrolling is handled incredibly well with HTMX and Go. HTMX enhances HTML in a way that allows AJAX requests and lazy loading without the need for JavaScript, while Go efficiently serves dynamic content. Together, they provide an effortless but powerful means to build smooth and responsive applications.

In this post, we’ll show how to implement infinite scrolling in a Go-based web app using HTMX, exploring its benefits and trade-offs along the way. By the end, you’ll have a solid understanding of how to create a seamless browsing experience that keeps users engaged and satisfied.

Why Infinite Scrolling?

Infinite scrolling improves user experience by eliminating the need for manual pagination controls. New content is dynamically loaded when the user reaches the end of the page, rather than by clicking “Next” or “Load More”. It is widely implemented in social media feeds, news sites, and e-commerce sites where continuous engagement is key.

Advantages of Infinite Scrolling

  • Smooth Navigation: The user can browse content continuously, emulating the app scrolling experience.
  • Better Data Fetching: Instead of loading everything all at once, infinite scrolling loads data in batches, reducing initial page loads.
  • Enhanced Engagement: Users spend more time on the website if content is being loaded dynamically.
  • Modern UX Expectations: Many popular platforms (Twitter, Instagram, YouTube) use infinite scrolling and thus create user expectations.

Challenges and Considerations

Although infinite scrolling has a lot of advantages, it is not always the right choice. Some of its probable downsides are:

  • Performance Problems: Without proper memory management, the repeated adding at the end to the DOM could slow down the browser.
  • SEO Limitations: Browsers won’t be able to index dynamically loaded content unless other methods (e.g., server-side rendering) are employed.
  • No Control for Users: Infinite scrolling does not have any control for users to jump to a particular page or item, which can be annoying at times.
  • Loading Delays: Some users may find it annoying to wait for server response with each scroll. They would prefer only one big wait to load everything at once.

Infinite scrolling may not be a good choice for users who have intermittent internet connectivity or who experience high server latency, as disruptions can ruin the experience.

Moreover, infinite scrolling breaks the scroll bar by causing it to display the page length inaccurately. Believe it or not, some people still use the scroll bar. People rely on the scroll bar to see how much is left to read. Sometimes, it’s not nice to tell people that they’re almost done when they’re not [1]. It is crucial to know your audience when deciding between infinite scrolling, pagination or any other alternative.

That being said, infinite scrolling is a powerful tool when used correctly. It can significantly improve user experience and engagement, especially for applications with large dynamic content feeds. Let’s see how it works and how to implement it with Go and HTMX.

How Infinite Scrolling Works

The infinite scrolling mechanism includes three important steps:

  1. Discover when the user reaches the bottom of the page.
  2. Fetch additional content from the server.
  3. Insert the new content in the existing list without a whole page reload.

This is typically done with JavaScript by monitoring the scroll position and calling the API when necessary. However, HTMX simplifies that with declarative attributes that can handle these interactions without writing any custom JavaScript.

When designing for Infinite Scroll, it’s essential to prioritize the Principles of Good Design.

Using HTMX for Infinite Scrolling

HTMX is a light JavaScript library that extends HTML to provide dynamic interactions without coding JavaScript. We can fetch and update content directly using attributes such as hx-get, hx-trigger, hx-swap and hx-target. Infinite scrolling can be implemented with HTMX with little effort by taking advantage of built-in triggers that determine when a user scrolls to a specific scroll position.

Implementing Infinite Scrolling with Go and HTMX

To demonstrate this, we’re going to build an employee listing with infinite scrolling. As the user scrolls, new employees will be fetched and displayed automatically without reloading the page.

For this implementation, we’ll be using:

  • Go to power the backend, handling HTML rendering and API responses.
  • HTMX to enable dynamic content updates without page reloads.
  • Leapkit to simplify web development with essential Go packages.
  • Tailwind CSS to create a clean, modern, and fully responsive layout.

With these technologies working together, we can build a smooth and efficient infinite scrolling experience. Let’s get started! 🚀

Setting Up the project

As we mentioned, we’ll use Leapkit to simplify the Go web development process. Leapkit provides a set of packages that make it easy to build web applications with Go. To get started, install Leapkit by running the following command:


$ go run rsc.io/tmp/gonew@latest go.leapkit.dev/template@latest infinite-scroll

This command will create a new project named infinite-scroll with the necessary project structure and dependencies.

Creating the Employee Model

Let’s start by creating a simple employee model. Create a new file named employee.go in the internal/employees directory.


// internal/employees/employee.go
package employees

type Employee struct {
	Name        string
	Title       string
	Description string
	Tags        []string
}

type Employees []Employee

This model defines an employee struct with fields for the employee’s name, title, description, and tags. We also define an employees type as a slice of employee structs.

Next, let’s create mock data to simulate a list of employees in our application.

Creating the Service

Create a new file named service.go in the internal/employees directory. This service will help us to fetch employees from the mock data.


// internal/employees/service.go
// Mock 50 employees with different roles and skills.
var employeesList = Employees{
	{"Alice Johnson", "Software Engineer", "Passionate about backend development.", []string{"Go", "Microservices", "Cloud"}},
	{"Bob Smith", "Frontend Developer", "Loves creating beautiful UI.", []string{"React", "CSS", "TypeScript"}},
	{"Charlie Brown", "DevOps Engineer", "Ensures smooth CI/CD pipelines.", []string{"Docker", "Kubernetes", "Terraform"}},
	{"David Wilson", "Product Manager", "Drives product vision and strategy.", []string{"Agile", "User Research", "Roadmaps"}},
	{"Eve Adams", "Data Scientist", "Expert in AI and ML.", []string{"Python", "TensorFlow", "Data Analysis"}},
	{"Frank White", "Security Engineer", "Focuses on cybersecurity best practices.", []string{"Penetration Testing", "Cryptography", "Compliance"}},
	{"Grace Hall", "UX Designer", "Creates intuitive and accessible designs.", []string{"Figma", "User Experience", "Prototyping"}},
	{"Henry King", "System Administrator", "Manages IT infrastructure.", []string{"Linux", "Networking", "Virtualization"}},
	{"Isabel Scott", "Software Architect", "Designs scalable software solutions.", []string{"Go", "Distributed Systems", "Architecture"}},
	{"Jack Lee", "QA Engineer", "Ensures software quality through testing.", []string{"Selenium", "Test Automation", "CI/CD"}},
    //...
}

Creating the Employee Listing Page

Let’s start by creating a simple employee listing page. First, create a new file named index.go.


// internal/employees/index.go
package employees

import (
	"net/http"
	"strconv"

	"go.leapkit.dev/core/render"
	"go.leapkit.dev/core/server"
)
func Index(w http.ResponseWriter, r *http.Request) {
    rw := render.FromCtx(r.Context())
    rw.Set("employees", employeesList)
    err := rw.Render("employees/index.html")
    if err != nil {
        server.Errorf(w, http.StatusInternalServerError, "error rendering template: %s", err.Error())
    }
}

This handler will render the template employees/index.html when the user visits the / route.

Next, create the index.html template file in the internal/employees directory.


<!-- internal/employees/index.html -->
<header class="mb-10">
    <h1 class="text-4xl font-bold text-gray-800">Team Members</h1>
    <p class="text-gray-600 mt-2">Explore our talented professionals</p>
</header>

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
    <%= partial("employees/list.html") %>
</div>
As you can see, this template includes a header with the title and description of the page, as well as a partial template employees/list.html that will display the list of employees.

Next, create the list.html partial template in the internal/employees directory.


<!-- internal/employees/list.html -->
<%= for (i, employee) in employees { %>
    <div class="bg-white rounded-xl shadow-md  hover:shadow-lg transition-all duration-300 flex flex-col h-full">
        <div class="h-24 bg-gradient-to-r from-purple-500 to-indigo-600 rounded-t-xl"></div>
        <div class=" p-6 flex flex-col gap-4 flex-grow">
            <div>
                <h3 class="text-xl font-bold text-gray-900"><%= employee.Name %></h3>
                <p class="text-indigo-600 font-medium"><%= employee.Title %></p>
                <p class="text-gray-500 text-sm mt-2"><%= employee.Description %></p>
                <div class="mt-4 flex gap-2 flex-wrap">
                    <%= for (tag) in employee.Tags { %>
                        <span class="px-2 py-1 text-xs rounded-full bg-indigo-100 text-indigo-800"><%= tag %></span>
                    <% } %>
                </div>
            </div>
            <div class="mt-auto flex justify-between items-center">
                <a href="/" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium">View Profile</a>
            </div>
        </div>
    </div>
<% } %> 

This list displays all employees, but loading a large number of records at once can cause performance issues. Here is where infinite scrolling comes into play.

Implementing Infinite Scrolling

To implement infinite scrolling, we need to load additional employees as the user scrolls down the page. We can achieve this using HTMX’s hx-get, hx-trigger, hx-swap, and hx-target attributes.

First, we need to create a function that returns a subset of employees based on a limit and offset. Add the following function to the service.go file.


// internal/employees/service.go
// employees retrieves a paginated list of employees from the employeesList array.
// The offset parameter determines where to start retrieving employees from the array.
// The limit parameter specifies the number of employees to display per page.
func employees(offset, limit int) Employees {
    // If the offset is equal to the length of the array, it means there are no more records to fetch.
    if offset >= len(employeesList) {
        return Employees{}
    }

    // Calculate the end index for the page.
    // min returns the minimum of two integers.
    end := min(offset+limit, len(employeesList))

    return employeesList[offset:end]
}

Next, update the Index handler to include the offset and limit parameters.


// internal/employees/index.go
// Index renders the home page of the application, displaying a paginated list of employees.
func Index(w http.ResponseWriter, r *http.Request) {
	rw := render.FromCtx(r.Context())

	// Offset is a parameter used for pagination. It determines where to start retrieving employees from the employeesList array.
	offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
	if offset < 0 {
		offset = 0
	}

	limit := 15                  // Number of employees to display per page.
	nextOffset := offset + limit // Calculate the next offset for pagination.

	total := len(employeesList)          // Get the total number of employees.
	hasMoreRecords := total > nextOffset // Determine if more employees exist beyond the current page.

	// Fetch the employees for the current page.
	employees := employees(offset, limit)

	rw.Set("employees", employees)
	rw.Set("hasMoreRecords", hasMoreRecords)
	rw.Set("nextOffset", nextOffset)

	// Check if the request is an HTMX request (partial rendering)
	if r.Header.Get("HX-Request") != "" {
		err := rw.RenderClean("employees/list.html")
		if err != nil {
			server.Errorf(w, http.StatusInternalServerError, "error rendering list template: %s", err.Error())
			return
		}
		return
	}

	// Render the full page template for normal requests.
	err := rw.Render("employees/index.html")
	if err != nil {
		server.Errorf(w, http.StatusInternalServerError, "error rendering template: %s", err.Error())
	}
}

In this updated handler, we calculate the offset and limit based on the query parameters. We then fetch the employees for the current page using the employees function. We also determine if there are more records available beyond the current page.

If the request is an HTMX request (partial rendering), we render the employees/list.html template without the full page layout. Otherwise, we render the full page template.

Next, update the list.html template to include the infinite scrolling functionality.


<!-- internal/employees/list.html -->
 <%= for (i, employee) in employees { %>
    <div class="bg-white rounded-xl shadow-md  hover:shadow-lg transition-all duration-300 flex flex-col h-full">
        <div class="h-24 bg-gradient-to-r from-purple-500 to-indigo-600 rounded-t-xl"></div>
        <div class=" p-6 flex flex-col gap-4 flex-grow">
            <div>
                <h3 class="text-xl font-bold text-gray-900"><%= employee.Name %></h3>
                <p class="text-indigo-600 font-medium"><%= employee.Title %></p>
                <p class="text-gray-500 text-sm mt-2"><%= employee.Description %></p>
                <div class="mt-4 flex gap-2 flex-wrap">
                    <%= for (tag) in employee.Tags { %>
                        <span class="px-2 py-1 text-xs rounded-full bg-indigo-100 text-indigo-800"><%= tag %></span>
                    <% } %>
                </div>
            </div>
            <div class="mt-auto flex justify-between items-center">
                <a href="/" class="text-indigo-600 hover:text-indigo-800 text-sm font-medium">View Profile</a>
            </div>
        </div>
    </div>
<% } %>

<!-- If there are more records to load, this div is rendered -->
<%= if (hasMoreRecords) { %>
    <!-- What does this do? -->
      <!-- 1. Makes an HTMX request to load the next batch of employees -->
      <!-- 2. Uses the Intersection Observer API to detect when the div enters the viewport -->
      <!-- 3. Updates this div with the response from the server -->
      <!-- 4. Replaces the entire div with the new content -->

    <div
        hx-get="/?offset=<%= nextOffset %>"  
        hx-trigger="intersect"  
        hx-target="this"  
        hx-swap="outerHTML"  
    ></div>
<% } %>

In this updated template, we display the list of employees as before. However, we also include a div element that triggers an HTMX request when it becomes visible at the bottom of the page. This div element fetches the next batch of employees using the hx-get attribute and updates itself with the response from the server using the hx-swap attribute.

The hx-trigger="intersect" attribute uses the Intersection Observer API to detect when the div element enters the viewport. When this happens, an HTMX request is triggered to load the next batch of employees.

With that approach, we have implemented infinite scrolling in our application. Now, let’s test it out!

Running the Application

To run the application, execute the following command:


$ go tool dev

This command will start the development server, and you can access the application at http://localhost:3000.

Result

🎉 And that’s all!

As you scroll down the page, you should see additional employees being loaded automatically without the need to click a “Load More” button. If you want to see the full code, you can check out the GitHub repository 📌.

Conclusion

Infinite scrolling enhances the user experience for applications with large dynamic content feeds such as social media sites, product pages, or news sites. It eliminates the need to paginate and helps the users engage more by loading new content uninterruptedly. It’s important to understand that implementation of infinite scrolling depends on context. It is best for scenarios in which users are expected to browse continuously versus being intended for specific searches. Knowing when to implement infinite scrolling keeps the balance between engagement and accessibility.

References

  1. Infinite Scrolling: Pros and Cons
  2. Understanding the Impact of Infinite Website Scrolling on User Experience and Traffic
  3. Intersection Observer API
Share this post: