Custom styling with LiveView function component attributes

Image by Annie Ruygt

In this post we use function component attributes to add new CSS classes to our component’s default classes. Fly.io is a great place to run your Phoenix LiveView applications! Check out how to get started!

Problem

You’re writing a new function component that has default styling. You’ve defined default CSS classes in the component’s template, but you want it to have the flexibility to add a few others when calling the function.

How can we give a component some default CSS classes and still customize it to add our own for custom styling?

Solution

In LiveView 0.18 function component attributes were added to define the attributes that the function component expects to receive —either required or not. We can define the type of the attributes, and even default values if we want to.

Among the different types that we can use to define the component’s attributes, there are global attributes. By default, a :global attribute can accept a set of attributes that are common to all standard HTML tags —id, class, hidden, autocapitalize, etc— so we don’t need to individually and repeatedly specify these attributes on every component we define.

How can we use all this to customize our function component’s classes? let’s see it!

Attempt 1

We’re specifying an HTML attribute —class— and we want to set a default value for it. Based on what we just learned, a function component attribute of type :global sounds like just what we need!

We define a :rest attribute as global, and we define a map with the default :class values. Then we interpolate the :rest attributes in the <img> tag:

attr :rest, :global, default: %{class: "rounded-full"}

def user_logo(assigns) do
  ~H"""
    <img {@rest} />
  """
end

When calling the function component :user_logo we no longer need to specify any CSS classes, but we do need to give it the image’s source:

<.user_logo src={img.src}></.user_logo>

It would render something like this:

<img class="rounded-full" src="https://avatars...">

But what if we want to add some other CSS classes to the function component besides the default ones?

<.user_logo src={img.src} class="w-10 h-10"></.user_logo>

If we set our component’s default class using a global attribute like that, it’s just that: a default. If we specify a new class value when we call the component, the new value overrides the default.

It would render something like this:

<img class="w-10 h-10"  src="https://avatars...">

We can use the default CSS classes without sending anything to the component, or we can define our own from scratch. That’s great!

But we don’t have any way to add classes, and that’s not what we’re looking for!

Attempt 2 - It works!

Instead of using a structured global attribute for the default class, we can turn it around: put the desired default value into the function component template, and make our :class attribute a string with a default value of nil.

This way we can group both classes in a list, and interpolate them inside the class attribute of our HTML tag:

attr :class, :string, default: nil
attr :rest, :global

def user_icon(assigns) do
  ~H"""
    <img
      class={["rounded-full", @class]}
      {@rest}
    />
  """
end

Here we kept the global :rest attribute for any other HTML attributes we may want to set when we call the function, but which we don’t have default values for. In this example, to set the image’s :src path.

If we render the updated function component just as we did above, we keep the default classes, and the additional ones:

<img class="rounded-full w-10 h-10" src="...">

That’s all! now we can add as many classes as we want without losing the default ones.

Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix LiveView app close to your users. It’s really easy to get started. You can be running in minutes.

Deploy a Phoenix app today!

Discussion

We set out to have our components start with some basic CSS styles and then possibly extend them at the point where we use them. We succeeded in making our component flexible enough to be styled with extra classes when needed.

We looked at two ways of doing it and ended up with one we’re really happy with. Beyond just classes, we saw how we can pass arbitrary HTML attributes through to our component as well!

We also saw how our components and their attributes allow us to describe our components. We can easily define the style, structure and functionality of our components with a clear and easy to use API. That’s pretty cool!