Psst. It would be super cool if you could try the new Phoenix Boilerplate

Try now →


View on Github

Combine Phoenix LiveView with Alpine.js


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

There is no need to keep the state of a dropdown on the server so you might just as well do it on the client.

However, there is still no fun to start writing javascript files but there is a library that have recently gained a lot of traction. Its called Alpine.js. Just like Tailwind it just relies in an enriched DOM.

An easy example for tabs would look like:

<div x-data="{ tab: 'foo' }">
    <button :class="{ 'active': tab === 'foo' }" @click="tab = 'foo'">Foo</button>
    <button :class="{ 'active': tab === 'bar' }" @click="tab = 'bar'">Bar</button>

    <div x-show="tab === 'foo'">Tab Foo</div>
    <div x-show="tab === 'bar'">Tab Bar</div>

However, to use Alpine with Phoenix LiveView, you might run into some issues. In this tutorial, I will try to show you how to overcome those issues.

STEP 1 - Install Alpine

First step is to install Alpine js.

cd assets && yarn add alpinejs

And the only change required in app.js is to simply require it:

import 'alpinejs'

STEP 2 - Add components

For these examples, I am using Tailwind UI. There are some preview examples on there page that includes both a drop down and a modal. Tailwind UI

The main caveat comes from the fact that LiveView renders twice. First when the server renders the page and the second time when the LiveView web socket connects to the server. When that happens, Alpine loses track of the DOM element.

There are two possible solutions for this. One is to only render the DOM element after the socket has connected. LiveView provides a helper for this:

<%= if connected?(@socket) do %>
    <div x-data="{ open: true }" >
<% end %>

So, the drawback with this approach is that the page will flicker. Because the button or whatever element wont be there on initial rendering.

To solve this I can either create a ghost element that is only visible when not connected:

<%= if connected?(@socket) do %>
  <div x-data="{ open: true }" >
<% else %>
  <div id="unconnected-id">
<% end %>

NOTE that it requires a unique id, otherwise LiveView js will catch an error.

That approach seems like it would have a lot of extra markup but might make sense if you also load actual data after the web socket has connected.

Another solution to this is to ask LiveView not up update the DOM element with a phx-update="ignore" but that might or might not work for you if you have dynamic content inside.

The other solution is to conditionally render an id. Like:

<div id="<%= if connected?(@socket), do: "connected-id", else: "not-connected-id" %>" x-data="{ open: true }" >

So, entire code would look like:

<div class="flex justify-end">
  <div id="<%= if connected?(@socket), do: "connected-id", else: "not-connected-id" %>" x-data="{ open: false }" @keydown.escape="open = false" @click.away="open = false" class="relative inline-block text-left">
      <span class="rounded-md shadow-sm">
        <button @click="open = !open" type="button" class="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800 transition ease-in-out duration-150">
          <svg class="-mr-1 ml-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
            <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
    <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
      <div class="rounded-md bg-white shadow-xs">
        <div class="py-1">
          <a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">Edit</a>
          <a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">Duplicate</a>
        <div class="border-t border-gray-100"></div>
        <div class="py-1">
          <a href="#" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">Delete</a>

And the result:

Related Tutorials

Published 01 Apr

Create a reusable modal with LiveView Component


To reduce duplicity and complexity in your apps, Phoenix LiveView comes with the possibility to use reusable components. Each component can have its..

Published 13 Feb

Tagging interface with Phoenix LiveView and Tailwind - Tagging part 2


In the previous tutorial, I set up the the backend for being able to add tags to products. I have also written a tutorial about adding a LiveView an..