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.
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.
Categories
Collection
Part of the Hotwire and Rails collection
Products and courses
-
Hello Hotwire
A course on Hotwire + Ruby on Rails.
-
Hello Rails
A course for newcomers to Ruby on Rails.
-
Rails UI
UI templates and components for Rails.