Built with jQuery and CoffeeScript, this page reflects the web development practices of its time rather than the Hotwire-based approach I use today.

Introduction

Loading buttons are a common user-experience concept used to indicate the state between starting and completing an action. They are most useful when server-side logic decides what happens next, such as displaying validation errors or a success message.

Single page applications may bypass loading buttons to appear more responsive since they use client-side logic, but loading buttons still play a large role in the standard MVC Rails application.

represents an action for the user to perform.

indicates the action in processing via a loading icon and disabled button.

Demo

Click the buttons below for a live demonstration.

This webpage is not hosted by Rails, so the above UX is just a simulation.
A "Log out" button would normally redirect you to the sign-in page.

How it works

  • A Rails helper method renders a button with data-remote=true and data-loading=true attributes.
  • data-remote=true instructs jquery_ujs.js to begin an ajax request.
  • data-loading=true instructs a jQuery listener to disable the button and insert a spinner.
  • Your Rails application determines what happens after the request finishes:
    • Reset the button's state
    • Redirect to another page

The Code

# config/routes.rb
resources :sessions, only: %w[new destroy]
# controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def destroy
    reset_session
  end
end
# helpers/sessions_helper.rb
module SessionsHelper
  def link_to_log_out
    link_to_with_icon(
      "log-out",
      "Log out",
      url,
      method: "DELETE",
      remote: true,
      class: "btn btn-primary",
      data: { loading: true },
    )
  end
end
# helpers/layout_helper.rb
module LayoutHelper
  def icon_tag(name, extra_class = nil)
    lucide_icon(name, class: ["icon", extra_class].compact.join(" "))
  end

  def icon_with_spinner(name)
    idle = content_tag(:span, icon_tag(name), data: { icon: "idle" })
    spinner = content_tag(:span, icon_tag("loader-circle", "icon-spin"), class: "hidden", data: { icon: "loading" })
    idle + spinner
  end

  def link_to_with_icon(icon, title, url, options = {})
    title_with_icon = icon_with_spinner(icon) << " ".html_safe << h(title)
    link_to(title_with_icon, url, options)
  end
end
# Gemfile
gem "jquery-rails"
gem "lucide-rails"
// assets/javascript/application.js
//
// jquery_ujs allows us to use 'data-remote',
// 'data-type', and 'data-method' attributes
//
//= require jquery
//= require jquery_ujs
//= require loading
# assets/javascripts/loading.coffee
$ ->
  # Insert a spinner and disable the button
  $(document).on('click', 'a[data-loading]', (e) ->
    $(this).attr('disabled', 'disabled')
    $(this).find('[data-icon=idle]').addClass('hidden')
    $(this).find('[data-icon=loading]').removeClass('hidden')
  )
<%# views/layouts/application.html.erb %>
<%= link_to_log_out %>
/* views/sessions/destroy.js.erb */
Turbolinks.visit("<%= new_session_path %>");

Wrap-Up

The Rails code is rather boring, but two important user-experience concepts occur on the frontend:

  1. Disabling the button prevents duplicate submissions.
  2. Using a loading spinner lets the user know we started performing the action.

For further reading, my AJAX Toggle Buttons article builds on these concepts to implement an on/off feature for buttons.