Three components for icons: <.icon> (smart dispatcher),
<.faicon> (Font Awesome wrapper), and
<.heroicon> (inline-SVG Heroicons). All share the same
attribute surface: name, class,
color, size, variant,
fill, stroke, title,
aria_label.
<.icon> — smart dispatcher
Routes by name prefix: "hero-X" → inline SVG via
<.heroicon>; anything else → <i class>
(works for Font Awesome, Bootstrap Icons, Lucide-font, etc.).
Heroicons branch
<.icon name="hero-rocket-launch" />
FA / class-based branch
<.icon name="fa-solid fa-rocket" />
Empty / nil
<.icon name={@maybe_nil} />
Renders nothing — useful when the icon is data-driven and may be absent.
<.heroicon> — all 25 curated icons
Outline variants from
heroicons.com
,
embedded as inline SVG with stroke="currentColor" so they
inherit the parent text color.
rocket-launchchart-barbriefcaseuser-groupcog-6-toothuserarrow-right-on-rectangleplususer-plusarrow-down-traypenciltrashshopping-cartbookmarkpaint-brushchart-bar-squarehomelist-bulletview-columnspencil-squarelock-closedbellswatchsquares-2x2book-open<.faicon> — Font Awesome wrapper
Decomposes name + variant so callers don't have to remember the
fa-solid / fa-regular / fa-light /
fa-brands class prefixes. Requires Font Awesome CSS to be
loaded.
Solid (default)
<.faicon name="rocket" />
Regular
<.faicon name="bell" variant="regular" />
Brands
<.faicon name="github" variant="brands" />
Pass-through attrs (color, size, fill, stroke, title, aria_label)
Every named attr declared on the component flows through to the rendered element. Phoenix.Component validates each at compile time — a typo at the call site becomes a warning instead of a silently dropped attribute.
Title (tooltip) and aria_label
<.heroicon name="bell" title="Notifications" aria_label="Notifications" />
Stroke override
<.heroicon name="bookmark" stroke="red" />
Fill override
<.heroicon name="bookmark" fill="currentColor" />
Why <.icon> exists — the string-attr pattern
Library chrome components like <.sidebar_item>,
<.button>, <.flash>, and
<.profile_nav_item> take a single icon
string and don't know whether it's an FA class or a heroicon name. They
delegate the decision to <.icon>.
<.sidebar_item label="Dashboard" icon="hero-chart-bar-square" href="/" /> <.sidebar_item label="Profile" icon="fa-solid fa-user" href="/profile" />
Both calls work because <.sidebar_item> renders
<.icon name={@icon} /> internally — heroicons go
through the SVG branch, FA strings through the <i class> branch.
Custom icon set via :icon_callback (Lucide demo)
Most projects standardize on one icon set — a custom SVG sprite folder,
a special font, a base64 sprite map, etc. Wire a single callback in
config.exs and <.icon> routes every
non-hero- name through it.
1. Configure the callback
# config/config.exs
config :keen_pure_admin,
icon_callback: {MyAppWeb.Icons, :render}
2. Define the function component
The callback receives the full assigns map: name,
class, color, size,
size_value (resolved from size or the
configured default), variant, fill,
stroke, title, aria_label.
Pattern-match on name to handle multiple icon sets.
defmodule MyAppWeb.Icons do
use Phoenix.Component
# Lucide SVGs saved at priv/static/assets/icons/lucide/*.svg.
# Sizing goes through `style` (not width/height attrs) because
# HTML <img> attrs require integer pixels — "1.75rem" would
# parse as "1" and render at 1px wide.
def render(%{name: "lucide-" <> file} = assigns) do
assigns = assign(assigns, :file, file)
~H"""
<img
src={"/assets/icons/lucide/\#{@file}.svg"}
style={"width: \#{@size_value}; height: \#{@size_value};"}
class={@class}
alt={@aria_label || @file}
title={@title}
/>
"""
end
# Fall through to FA-style for anything else
def render(assigns), do: ~H"""<i class={[@name, @class]} />"""
end\
3. Use it
<.icon name="lucide-rocket" />
This demo ships 8 Lucide SVGs under
demo/priv/static/assets/icons/lucide/. Each tile below
is a real <.icon name="lucide-X" /> routed through
the configured callback:
lucide-rocketlucide-belllucide-settingslucide-userlucide-houselucide-searchlucide-pluslucide-trash
Note: rendering Lucide via <img> loses
stroke="currentColor" recoloring — for full CSS color
inheritance you'd inline the SVG (read at compile time, or read +
cached at request time).
When to use which component
| Component | When |
|---|---|
<.icon>
| You have a string and don't know its type (e.g. attr-driven, config-driven). Used internally by library chrome. |
<.faicon>
| Direct FA use with explicit variant control. Cleanest when you know it's FA. |
<.heroicon>
| Direct Heroicon use, no name-prefix ceremony. Cleanest when you know it's a heroicon. |