Handle LiveView Streams empty state with Tailwind CSS

Currently, in September 2023, LiveView does not offer an easy way to check if the @streams does not contain elements. The information is essential if we want a different UI for our empty state. After some research, I saw a post from “codeanpeace” on the Elixir Forum proposing using some Tailwind helpers. I decided to give it a try, and it works pretty great.

The final solution will be like this:

Initially, we show the empty state, and then after creating a record, the UI changes and displays the first record of the list without the empty state block.

Now, let’s dive into the code. Our list is very straightforward. We added a boards key with the list of “boards” to our stream and will use this later in the html.

defmodule ReflectiveLoopWeb.BoardLive.Index do
  use ReflectiveLoopWeb, :live_view
  alias ReflectiveLoop.Retro

  @impl true
  def mount(_params, _session, socket) do
    {:ok, stream(socket, :boards, Retro.list_boards())}
  end
  # ...
end

I use a custom component to display a list that applies my desired style. I’ve omitted the non-essential parts. The critical factor is the render_slot(@new_item_action). Notice that it’s inside the div that will have the items from the @streams. The stacked_list component doesn’t know about the style of the slot, so we have the freedom to have lists with or without a custom empty state.

  def stacked_list(assigns) do
    ~H"""
    <div id={@id}>
      <div phx-update="stream" id="board-list">
        <%= render_slot(@new_item_action) %>

        <div :for={{dom_id, line} <- @lines} id={dom_id} role="list">
          <div>
            <p>
              <a href="#">
                <%= render_slot(@title, line) %>
              </a>
            </p>
          </div>
        </div>
      </div>
    </div>
    """
  end

Now, for the html part, we will use some pseudo-classes to change the style of the new item button, considering if the list is empty or not. I opted to use a single link with some extra css applied when it is the only element in the list.

An image showing the two possible states for the new board button

Let’s check how the component works. Again, I’ve omitted the non-essential parts. As you can see, it’s straightforward. What makes the “magic” is the only pseudo-class that I’ve applied in the link.

<.stacked_list
  id="boards"
  lines={@streams.boards}
>
  <:title :let={board} label="name">
    <%= board.name %>
  </:title>

  <:new_item_action>
    <.link
      id="new-action-button"
      patch={~p"/boards/new"}
      class="only:block only:relative only:w-full only:rounded-lg only:bg-white only:border-2 only:border-dashed only:border-gray-300 only:p-12 only:text-center"
    >
      <.icon name="hero-inbox-stack-solid" class="text-gray-900" />

      <span class="pl-1 text-sm font-semibold text-gray-900">
        Create a new board
      </span>
    </.link>
  </:new_item_action>
</.stacked_list>

The structure needs to have a specific format. We have a div with two children, and we use Tailwind to display extra css when the link is the only child.

<div phx-update="stream">
  <a></a>
  <div></div>
</div>

But there is an a and a div, so we always have two children, no? Actually, no. The tag is not rendered when no items are in the @streams. In the empty state, the HTML will be a single child tag.

<div>
  <a></a>

</div>

I have never considered using CSS to handle states, but this works well with streams. If you don’t use Tailwind, style your div with :only-child, and you have the same result.

Stay up to date!

Get notified when I publish something new, and unsubscribe at any time.