Phoenix LiveView Tailwind Variants

A person clutching an open laptop to their chest, face-out so we can read the screen, which just says 'HTML'. Little love-hearts emanate from the screen. There's a ribbon spiralled loosely around person and computer, with 'CSS' printed repeatedly along its length.
Image by Annie Ruygt

Problem

Users of Tailwind CSS know the productivity gains the utility-first CSS framework provides. One of Tailwind's biggest advantages is that you can rapidly build applications without ever leaving your HTML. There's no context-switching between markup and CSS files, or searching for where classes are or aren't defined.

You almost never have to touch individual CSS files – except when you want to customize the styling of the classes that Phoenix LiveView uses for loading indicators and form feedback. Yuck.

Worse than leaving your markup to trudge through CSS files, is the strict nature of the Phoenix LiveView classes that make them hard to customize without many one-off CSS rules.

Fortunately, Tailwind has a plugin feature that solves this beautifully.

Solution

Phoenix LiveView uses the following CSS classes to provide user feedback:

  • phx-no-feedback - applied when feedback should be hidden from the user
  • phx-click-loading - applied when an event is sent to the server on click while the client awaits the server response
  • phx-submit-loading - applied when a form is submitted while the client awaits the server response
  • phx-change-loading - applied when a form input is changed while the client awaits the server response

Customizing each of these for each scenario, breakpoint, desktop and mobile size, etc, gets cumbersome and error-prone. We can sidestep these issues by defining a Tailwind plugin which provides variants for each of these Phoenix LiveView specific classes inside your assets/tailwind.config.js file:

const plugin = require('tailwindcss/plugin')

module.exports = {
  content: [
    "./js/**/*.js",
    "../lib/*_web.ex",
    "../lib/*_web/**/*.*ex"
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require("@tailwindcss/forms"),
    plugin(({addVariant}) => addVariant('phx-no-feedback', ['&.phx-no-feedback', '.phx-no-feedback &'])),
    plugin(({addVariant}) => addVariant('phx-click-loading', ['&.phx-click-loading', '.phx-click-loading &'])),
    plugin(({addVariant}) => addVariant('phx-submit-loading', ['&.phx-submit-loading', '.phx-submit-loading &'])),
    plugin(({addVariant}) => addVariant('phx-change-loading', ['&.phx-change-loading', '.phx-change-loading &']))
  ]
}

We added four plugin definitions which add variants for each Phoenix LiveView class. The &.phx-click-loading notation specifies that the variant is applied when an element directly has the phx-click-loading class applied. Additionally, the .phx-click-loading & notation tells Tailwind to also apply the variant to children of a container with the phx-click-loading class.

This allows us to customize our markup like so:

<button
  phx-click="send"
  phx-disable-with="Sending..."
  class="p-4 rounded-lg bg-indigo-600 phx-click-loading:animate-pulse"
>
  Send!
</button>

We style our button with the regular Tailwind classes, then we customize which Tailwind classes are applied on phx-click. We do this by simply prefixing the Tailwind classes by phx-click-loading, such as phx-click-loading:animate-pulse to show animation feedback while the server processes our event. Here's what it looks like in action:

You can expand this to apply red borders to form inputs when the inputs are invalid, or hide and show input errors – all without leaving your markup. Since elements are all stylized inline, you can also customize your Phoenix LiveView feedback on a case-by-case basis. Happy hacking!