Hotwire Turbo Frames: Add Seamless Loading Spinners in Rails

Imagine we’re building a web application to organize our personal trip albums. We could have a list of albums, and when you click on each album, a grid of photos would be displayed.

The HTML layout might look something like this:

<aside>
  <h2>My Trips</h2>
  <ul>
    <% @albums.each do |album| %>
      <li>
        <!-- 
          When an album is clicked, the photos will be displayed
          inside a Turbo Frame
        -->
        <%= link_to album.name, album_photos_path(album), 
          data: { turbo_frame: 'album' } %>
      </li>
    <% end %>
  </ul>
</aside>

<%= turbo_frame_tag 'album' do %>
  <p>Select an album to view the photos</p>
<% end %>

The links in the sidebar include the data-turbo-frame attribute to use Turbo for fetching the photos. The layout for displaying the photos could look something like this:

<%= turbo_frame_tag "album" do %>
  <div class="photos">
    <% @photos.each do |photo| %>
      <div class="photo">
        <%= image_tag photo.url %>
      </div>
    <% end %>
  </div>
<% end %>

It’s important to use the same Turbo Frame ID that we used in the main layout. In this case, the ID is album . Let’s test this out:

The pictures are displayed using Turbo, but since it’s a heavy request, the delay between clicking an album and displaying the photos makes the app feel a bit clunky. To improve the user experience (UX), we could add a spinner or some indicator to alert the user that the album is loading.

We can do this with Javascript using Stimulus controller, but there’s a simpler and more efficient way, using CSS. Every time a Turbo Frame is loading, a busy attribute is added to the Turbo Frame tag.

As explained in the documentation: busy is a boolean attribute toggled to be present when a <turbo-frame>-initiated request starts, and toggled false when the request ends


With this, we can add some CSS classes and display an image with a spinner when the busy attribute is added:

#album {
  position: relative;
}

#album[busy]::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(255, 255, 255, 0.8);
  display: flex;
  align-items: center;
  justify-content: center;

  background-image: url("spinner.svg");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 50px 50px
}

Now, when we click on an album, a spinner is displayed. Isn’t that great?

With these improvements, we’ve made the user experience smoother and more intuitive by showing a spinner while the album is loading. This small but impactful feature helps keep users informed and engaged, making the app feel faster and more responsive. By leveraging Turbo and some simple CSS, we’ve enhanced the interactivity of our app with minimal effort.