Customizing Topbar to Add a Delay Before Showing Page Loading Progress
We're going to look at a Phoenix LiveView app for this example but this applies to any web app using any web framework.
It’s a nice UI enhancement to add a little progress bar at the top of a page when it’s loading. Lots of sites do this like GitHub, YouTube and more. I’m sure you’ve noticed a little bar that loads at the top of these sites as you transition between pages.
When you build a site using pjax,
Turbolinks, Hotwire
Turbo, htmx,
LiveView or a SPA
(single page app) you can get the benefit of being able to do very fast
page transitions where at the very least you only swap the <body></body>
of
the page instead of doing a full reload.
Depending on which tech you use, you can also have more fine grained control such as only partially updating a tiny area of the page that needs to change but that’s not an important detail for now.
Personally every site I’ve built since 2016 or so has used one of the above technologies. Mainly Turbolinks and now Hotwire Turbo which is its successor. An example of one is my Running in Production podcast site which uses Turbolinks.
In any case, if you’re using Phoenix, out of the box it will use a JS library called Topbar to control rendering this progress bar. nProgress is another option that used to be very popular. Both of these libraries work with any web framework.
# Delaying Topbar Will Make Your Pages Feel Faster
The video below is going to show how you can add an initial delay to Topbar so it only shows up for responses that take longer than 500ms. This is what Hotwire Turbo does by default but Topbar doesn’t have this feature (at least not at the time of making this video).
Even if Phoenix LiveView decides to use something else in the future this same concept applies. You can use exactly the same strategy with something other than Topbar.
In my opinion there is a huge win from doing this because when you show this progress bar on fast loading pages then it creates an illusion that the page is loading slower than it is really is due to the progress bar animation taking longer to finish than it takes for the page to load. We’ll see a before / after of this on video.
# Demo Video
Code snippets
After getting the docker-phoenix-example app running you’ll want to make the following changes, or if you already have a Phoenix app feel free to add these changes in or use your existing live views.
Add this to assets/js/app.js
(this is already in the repo, but it’s here for a reference):
import topbar from "topbar"
let topBarScheduled = undefined
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", (info) => {
if(!topBarScheduled) {
topBarScheduled = setTimeout(() => topbar.show(), 500)
}
})
window.addEventListener("phx:page-loading-stop", (info) => {
clearTimeout(topBarScheduled)
topBarScheduled = undefined
topbar.hide()
})
Add this to lib/hello_web/router.ex
:
live "/1", DemoLive.One, :index
live "/2", DemoLive.Two, :index
Create lib/hello_web/live/demo/one.ex
:
defmodule HelloWeb.DemoLive.One do
use HelloWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
def render(assigns) do
~H"""
<div class="pt-12">
<%= live_patch "Page Two", to: Routes.demo_two_path(@socket, :index) %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</div>
"""
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "One")
end
end
Create lib/hello_web/live/demo/two.ex
:
defmodule HelloWeb.DemoLive.Two do
use HelloWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
def render(assigns) do
~H"""
<div class="pt-12">
<%= live_patch "Page One", to: Routes.demo_one_path(@socket, :index) %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
</div>
"""
end
@impl true
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Two")
end
end
Create lib/hello_web/live/live_helpers.ex
:
defmodule HelloWeb.LiveHelpers do
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
end
Timestamps
- 0:10 – What is a site-wide progress indicator or topbar?
- 0:32 – This works with pjax, Turbolinks, Hotwire Turbo, htmx, LiveView or SPAs
- 1:03 – Not showing the topbar for fast responses ends up being a huge win
- 1:42 – Hotwire Turbo has a built in progress bar delay set to 500ms by default
- 2:49 – I brought this up with the creator of Elixir and he added it to the docs
- 3:43 – Going over the issue by looking at the before version of the app
- 6:13 – Implementing a bit of code to delay showing topbar and checking out the results
- 10:29 – Adding an artificial delay to make sure the progress bar still shows
- 11:40 – This delayed progress bar is nice to avoid seeing it during network blips too
- 12:24 – Wrapping things up, any questions?
References Links
- https://github.com/nickjj/docker-phoenix-example
- https://github.com/phoenixframework/phoenix_live_view/commit/22ac99b108a167c324182f22be369aeea9eff346
Which progress bar solution do you use? Let me know below.