Andy from Webcrunch

Subscribe for email updates:

Portrait of Andy Leverenz
Andy Leverenz

August 14, 2024

Last updated August 14, 2024

Custom progress bars with Rails and Hotwire

The Turbo Rails progress bar is a subtle yet powerful feature that enhances user experience by providing visual feedback during page loads.

By default, it appears as a slim blue bar at the top of the page for requests that take longer than 500ms. However, with a few simple tweaks, you can transform this element better to match your application's design and brand identity.

This post will cover the basics and give you an idea of how to add a completely custom loading experience via Stimulus and Tailwind CSS.

Understanding the Basics

The Turbo progress bar is essentially a <div> element with the class name turbo-progress-bar. It comes with default styles that you can easily override using CSS. Here are the default styles for reference:

.turbo-progress-bar {
  position: fixed;
  display: block;
  top: 0;
  left: 0;
  height: 3px;
  background: #0076ff;
}

Customization Techniques

Let's explore ways to customize the progress bar using Tailwind CSS @apply directives in Tailwind. You can use plain ole' CSS here all the same:

1. Change the Background Color

.turbo-progress-bar {
   @apply bg-yellow-500;
}

2. Add Rounded Corners

.turbo-progress-bar {
   @apply bg-blue-500 rounded-r-full;
}

3. Create a Glowing Effect

.turbo-progress-bar {
  @apply bg-blue-500 h-4 shadow-xl shadow-blue-500/90;
}

4. Gradient Fade

.turbo-progress-bar {
  @apply bg-gradient-to-r from-transparent to-sky-500;
}

5. Add a Gradient shift (movement)

@keyframes gradient-shift {
  0% {
    background-position: 0% 50%;
  }
  100% {
    background-position: 100% 50%;
  }
}

.turbo-progress-bar {
  background: linear-gradient(to right, #3498db, #2ecc71, #3498db);
  background-size: 200% auto;
  height: 16px;
  animation: gradient-shift 0.5s linear infinite;
}

Additional Tips

Hide the Progress Bar: If you don't want to show it, you can hide it with CSS. For some reason, this is the only way to disable it 🙃.

.turbo-progress-bar {
  visibility: hidden;
}
  • Adjust the Delay: To change when the progress bar appears (default is 500ms), use JavaScript:
Turbo.setProgressBarDelay(delayInMilliseconds)

A fully custom approach

A simple bar is likely enough for most apps. If you want a more branded experience, you could re-invent it using Turbo events, Stimulus.js, and a bit more Tailwind CSS and markup.

I’ll start by adding a new stimulus controller.

rails g stimulus loading_bar

Inside the file is the following code.

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["bar", "percentage"]

  connect() {
    this.hideBar()
    this.setupTurboListeners()
  }

  setupTurboListeners() {
    document.addEventListener("turbo:before-visit", this.showBar.bind(this))
    document.addEventListener(
      "turbo:before-fetch-response",
      this.startProgress.bind(this)
    )
    document.addEventListener("turbo:load", this.hideBar.bind(this))
  }

  showBar() {
    this.element.classList.remove("hidden")
    this.progress = 0 // Initialize progress here
    this.barTarget.style.width = "0%"
    this.percentageTarget.textContent = "0%" // Initialize percentage text
    this.interval = setInterval(() => this.updateProgress(), 100) // Start progress update interval
  }

  startProgress() {
    this.progress = 0 // Reset progress on visit
    clearInterval(this.interval) // Clear any existing interval
    this.interval = setInterval(() => this.updateProgress(), 100) // Start a new interval
  }

  updateProgress() {
    this.progress += Math.random() * 10
    if (this.progress >= 100) {
      this.progress = 100
      this.hideBar() // Stop the interval and hide the bar when complete
      clearInterval(this.interval)
    }
    this.barTarget.style.width = `${this.progress}%`
    this.percentageTarget.textContent = `${Math.round(this.progress)}%`
  }

  hideBar() {
    this.barTarget.style.width = "100%"
    setTimeout(() => {
      this.element.classList.add("hidden")
    }, 3000)
  }
}

Finally, in the layout, I added a new partial called "loader" and housed it in a new view directory called "shared".

<!DOCTYPE html>
<html>
  <head>
    <title>CustomTurboProgress</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
  </head>

  <body class="bg-zinc-950 text-white antialiased">
    <%= render "shared/loader" %>

    <div class="container mx-auto p-10">
      <%= yield %>
    </div>
  </body>
</html>

That partial contains the following code:

<div id="custom-loading-bar" class="hidden fixed inset-0 w-screen h-screen z-0 bg-black/40 backdrop-blur-md" data-controller="loading-bar">
  <div class="flex items-center flex-col h-full justify-center">
    <div class="flex items-center justify-center w-[180px] relative bg-black/90 py-9 px-1 rounded-lg border border-zinc-700/90 text-center">
      <div class="w-full px-6">
        <div class="flex justify-center items-center">
          <div class="progress-bar" data-loading-bar-target="bar"></div>
        </div>

        <div class="mt-2 flex-shrink-0 block w-full font-bold text-center" data-loading-bar-target="percentage">0%</div>
      </div>
    </div>
  </div>
</div>

Yes, this is way more involved than the default loader, but for good reason. We hook into Turbo events and bind to those to trigger animations, loading status, and more. Ultimately, I made a new UI that fixes a loader to the screen's center and outputs a percentage of the load.

To try this out, I threw a Ruby sleep 4 line on the show action of a PostsController file after generating a scaffold.

rails g scaffold Post title:string content:text

To add some data, I made a seed file and leveraged the faker gem (bundle add faker —include it in the development environment).

# seeds.rb

50.times do
  Post.create(title: Faker::Lorem.sentence(word_count: 3), content: Faker::Lorem.paragraph(sentence_count: 5))
end

And with that, we end up with something nifty. P.S. I went with a quick Tailwind CSS dark theme.

📺 View demo

Is this better than the default?

I’d argue the default is the better path. A custom UX like the one I presented could go far for taxing queries or longer page loads. The “loader” craze brought forth by front-end SPA-like apps left a bad taste in my mouth regarding loaders. They serve a purpose, but getting out of the end user's way will pay the most dividends in your applications.

Link this article
Est. reading time: 4 minutes
Stats: 492 views

Categories

Collection

Part of the Hotwire and Rails collection

Products and courses