View on Github

Create ghost loading cards in Phoenix LiveView

This post was updated 01 May - 2020


Unless you already didn’t know, when a LieView component is mounted on a page, it runs the mount/2 function twice. One when the page is rendered from the initial request, and one one when the socket is connected. So if you have data that is slow or resource heavy to load, you should consider to wait until the socket is connected so the intial load is not in vein.

A popular approach is to use ghost cards. A ghost gard is a loading placeholder that looks similar to the content that you try to load in but without actual data.

And you can test this by inserting a sleep in your code. For example a 3 seconds sleep is :timer.sleep(3000)

So, instead of a blank loading screen, in my example it would look something like this:

As it turns out, this is (of course) quite simple to do with Phoenix LiveView.

STEP 1 - Update LiveView component

I already have my products list in a LiveView component. And is this case its mounted from the rotes file and I actually load the data in the handle_params/3 function. And the function I call is the get_and_assign_page/1. I need to change that to take a second argument. If we are connected or not.

  # lib/tutorial_web/live/product_list_live.ex
  def handle_params(_, _, socket) do
    connected = connected?(socket)
    assigns = get_and_assign_page(nil, connected)
    {:noreply, assign(socket, assigns)}

Then I change the get_and_assign_page/1 do take two arguments like:

  # lib/tutorial_web/live/product_list_live.ex
  def get_and_assign_page(_page_number, false) do
    total_count = Repo.aggregate(Product, :count)
    product_count = Enum.min([10, total_count])

      products: Enum.to_list(1..product_count)

  def get_and_assign_page(page_number, _) do
      entries: entries,
      page_number: page_number,
      page_size: page_size,
      total_entries: total_entries,
      total_pages: total_pages
    } = Products.paginate_products(page: page_number)

      products: entries,
      page_number: page_number,
      page_size: page_size,
      total_entries: total_entries,
      total_pages: total_pages

NOTE In the first one, I just want to know how many ghost lines I want to display. And since there is pagination on page, I want to show a maximum of 10. If there is less than 10 products in the database, I want to show the correct amount of ghost lines. And to know that, I just count the records in the database with Repo.aggregate(Product, :count)

STEP 2 - Update the template

I think the easiest way (for me) is to solve it my rendering a different template. You might of course solve it otherwise.

So still in my LiveView component

  # lib/tutorial_web/live/product_list_live.ex
  def render(assigns) do
    if connected?(assigns.conn) do
      TutorialWeb.ProductView.render("products.html", assigns)
      TutorialWeb.ProductView.render("products_loading.html", assigns)

and then add the new loading template products_loading.html.leex:

<div class="card mb-20">
  <div class="card-header flex">
    <h5 class="mb-0 flex-1">Listing Products</h5>
    <span class="text-sm"><%= link "New Product", to: Routes.product_path(@conn, :new), class: "text-white hover:text-gray-200" %></span>
  <table class="card-body table">

<%= for _ <- @products do %>
        <td><div class="h-6 bg-gray-200"></div></td>
        <td><div class="h-6 bg-gray-200"></div></td>
        <td><div class="h-6 bg-gray-200"></div></td>
        <td class="w-1/12 whitespace-no-wrap"><div class="h-6 bg-gray-200"></div></td>
<% end %>


This would without any Javascript add a loading ghost card with pure LiveView. First it will display the loading card and then the actual list. It would switch from:


Phoenix Bolerplate

Generate a Phoenix Boilerplate and save hours on your next project.

Try now

SAAS Starter Kit

Get started and save time and resources by using the SAAS Starter Kit built with Phoenix and LiveView.

Subscribe for $39/mo to geat ahead!

Learn More

Related Tutorials

Published 04 May - 2021 - Updated 05 May

How to combine Phoenix LiveView with Alpine.js

No matter how great Phoenix LiveView is, there is still some use case for sprinking some JS in your app to improve UX. For example, tabs, dropdowns,..

Published 23 Dec - 2020

Getting Started with Phoenix and LiveView

This guide to getting started with Phoenix covers getting up and running with Elixir and Phoenix. This is a direct conversion of the Getting started..