LiveView Components

Phoenix LiveView function components and live components — attrs, slots, HEEx templates, and stateful components

Components are the building blocks of reusable UI in Phoenix LiveView. There are two types: function components (stateless) and live components (stateful).

Function components

Function components are pure functions that take an assigns map and return HEEx markup:

defmodule MyAppWeb.CoreComponents do
  use Phoenix.Component

  attr :label, :string, required: true
  attr :class, :string, default: nil
  attr :rest, :global

  def button(assigns) do
    ~H"""
    <button class={["btn", @class]} {@rest}>
      <%= @label %>
    </button>
    """
  end
end

Using a function component

<.button label="Save" phx-click="save" />
<.button label="Cancel" class="btn-secondary" />

Attrs

Attrs validate and document component inputs:

attr :name, :string, required: true
attr :age, :integer, default: 0
attr :role, :string, values: ["admin", "editor", "viewer"]
attr :active, :boolean, default: false
attr :items, :list, default: []
attr :user, :map, required: true
attr :class, :string, default: nil
attr :rest, :global  # captures remaining HTML attributes

Attr types

Type Description
:string String value
:integer Integer value
:float Float value
:boolean Boolean value
:atom Atom value
:list List value
:map Map / struct
:any Any type

Slots

Slots allow passing content blocks into components:

slot :header, doc: "Optional header content"
slot :inner_block, required: true

def card(assigns) do
  ~H"""
  <div class="card">
    <div :if={@header != []} class="card-header">
      <%= render_slot(@header) %>
    </div>
    <div class="card-body">
      <%= render_slot(@inner_block) %>
    </div>
  </div>
  """
end

Using slots

<.card>
  <:header>Important Notice</:header>
  This is the card content.
</.card>

Named slots with arguments

attr :items, :list, required: true
slot :item, required: true do
  attr :item, :map, required: true
end

def list_items(assigns) do
  ~H"""
  <ul>
    <li :for={item <- @items}>
      <%= render_slot(@item, item) %>
    </li>
  </ul>
  """
end
<.list_items items={@users}>
  <:item :let={item}>
    <%= item.name %> — <%= item.email %>
  </:item>
</.list_items>

Built-in form components

Phoenix generates a CoreComponents module with common components:

.simple_form

<.simple_form for={@form} phx-change="validate" phx-submit="save">
  <.input field={@form[:title]} label="Title" />
  <.input field={@form[:body]} type="textarea" label="Body" />
  <:actions>
    <.button>Save Post</.button>
  </:actions>
</.simple_form>

.input

<!-- Text input -->
<.input field={@form[:name]} label="Name" />

<!-- Email input -->
<.input field={@form[:email]} type="email" label="Email" />

<!-- Password input -->
<.input field={@form[:password]} type="password" label="Password" />

<!-- Textarea -->
<.input field={@form[:body]} type="textarea" label="Body" />

<!-- Select -->
<.input field={@form[:role]} type="select" options={["Admin", "Editor", "Viewer"]} />

<!-- Checkbox -->
<.input field={@form[:active]} type="checkbox" label="Active?" />

Live components (stateful)

Live components maintain their own state and life cycle. Use them for interactive, self-contained pieces of UI:

defmodule MyAppWeb.SortableHeaderComponent do
  use Phoenix.LiveComponent

  attr :field, :atom, required: true
  attr :current_sort, :atom, required: true
  attr :direction, :atom, required: true

  def render(assigns) do
    ~H"""
    <th class="cursor-pointer" phx-click="sort" phx-value-field={@field} phx-target={@myself}>
      <%= humanize(@field) %>
      <span :if={@current_sort == @field}>
        <%= if @direction == :asc, do: "↑", else: "↓" %>
      </span>
    </th>
    """
  end
end

Embedding a live component

<.live_component module={MyAppWeb.SortableHeaderComponent}
  id="sort-name"
  field={:name}
  current_sort={@sort_field}
  direction={@sort_dir} />

Live component life cycle

Callback Purpose
mount/1 Initialize socket assigns (no params)
update/2 Receive new assigns from parent
update/3 Receive new assigns + params from send_update
render/1 Render the component
handle_event/3 Handle events (use phx-target={@myself})
defmodule MyAppWeb.AccordionComponent do
  use Phoenix.LiveComponent

  @impl true
  def mount(socket) do
    {:ok, assign(socket, open: false)}
  end

  @impl true
  def update(assigns, socket) do
    {:ok, assign(socket, assigns)}
  end

  @impl true
  def handle_event("toggle", _, socket) do
    {:noreply, update(socket, :open, &!/1)}
  end

  @impl true
  def render(assigns) do
    ~H"""
    <div>
      <button phx-click="toggle" phx-target={@myself}>
        <%= if @open, do: "▼", else: "▶" %> <%= @title %>
      </button>
      <div :if={@open} class="panel">
        <%= render_slot(@inner_block) %>
      </div>
    </div>
    """
  end
end

Important: Events in live components must use phx-target={@myself} so the LiveView routes the event to the component instead of the parent.

send_update

Update a live component from outside its own events:

# From a parent LiveView or another process
Phoenix.LiveView.send_update(MyAppWeb.ModalComponent, id: "confirm-modal", show: true)

# The component receives it in update/3
@impl true
def update(%{show: show}, socket) do
  {:ok, assign(socket, show: show)}
end

HEEx template features

<!-- Conditional rendering -->
<div :if={@user}>
  Hello, <%= @user.name %>
</div>

<!-- Loop -->
<ul>
  <li :for={item <- @items}><%= item.name %></li>
</ul>

<!-- Dynamic attributes -->
<div class={["card", @active && "card-active"]} id={@id}>
  ...
</div>

<!-- Safe HTML interpolation -->
<span>{@raw_html_content}</span>

<!-- Attribute shorthand -->
<input type="text" {@extra_attrs} />

<!-- Dynamic tag -->
<.tag name={@as_header ? "h1" : "p"}>Content</.tag>