A different syntax for Phoenix LiveComponents

Functional live components

Monday, 11 May 2026

Categories

Even after writing Elixir and Phoenix for quite a few years, I’ve picked up some new tricks at Verna and I wanted to share my favourite one.

In Phoenix, there are two different syntaxes for rendering components. Functional components without any state can be defined like so:

defmodule MyAppWeb.GreetingComponent do
  use MyAppWeb, :html

  attr :name, :string, required: true

  def greeting(assigns) do
    ~H"""
      <h1>Hello {@name}</h1>
    """
  end
end

And then imported and rendered with:

import MyAppWeb.GreetingComponent, only: [greeting: 1]
# ...
<.greeting name="Rocky" />

A live component is slightly different and more verbose. Given a CountComponent that takes a prop of initial_count, we do it like this:

defmodule MyAppWeb.CountComponent do
  use MyAppWeb, :live_component

  attr :initial_count, :integer, required: true

  def render(assigns) do
    # HEEX
  end
end

And then rendered with this:

<.live_component
  module={CountComponent}
  id="count"
  initial_count={0}
/>

Phoenix is great, but I always found this difference in syntax a bit weird. It turns out that live components can actually be called using the simpler syntax. If we refactor our CountComponent, we can expose it as a function:

defmodule MyAppWeb.CountComponent do
  use MyAppWeb, :live_component

  attr :initial_count, :integer, required: true

  def count(assigns) do
    ~H"""
      <.live_component
        module={__MODULE__}
        id="count"
        initial_count={@initial_count}
      />
    """
  end

  def render(assigns) do
    # HEEX
  end
end

Wherever we want to use it, we can import it with:

import MyAppWeb.CountComponent, only: [count: 1]

And use it exactly like our functional component:

<.count initial_count={0} />

It’s definitely more verbose when defining a live component, but it also means that we can encapsulate the call to live_component all within a single file.