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.
How the icon system works
Pure Admin has two icon layers — know which one you're touching:
1. Masked .pa-icon primitive
The framework's closed set of structural affordances (close, chevrons,
success, danger, …) — a raw
<span class="pa-icon pa-icon--NAME"> painted in
currentColor via CSS mask. No component, no provider; it's how
the library's own chrome (alerts, flash, filter cards) draws glyphs.
Catalogued on
Design › Icons
.
2. The <.icon> component
A thin dispatcher over that primitive plus a swappable provider
system, so you render decorative / brand icons (Font Awesome, Heroicons,
Lucide, …) from a single <.icon name="…"> call-site —
without hand-authoring elements each time. That's this page.
Resolution order
A name flows through three stages — the first hit wins:
-
Framework affordance (reserved, built-in).
affordance?/1→<span class="pa-icon pa-icon--NAME">, then stop. No provider can shadow it; zero-config, always resolves. -
Provider list — each entry in
:icon_providers(seePureAdmin.Config), in order; the first to return markup (non-nil) wins. -
Built-in fallback —
"hero-X"→<.heroicon>; anything else → FA-style<i class={name}>.
What a provider is
A 1-arity Phoenix function component receiving the full assigns — the
name plus render context (class, color,
size, size_value, variant,
fill, stroke, title,
aria_label, is_interactive) — and returning rendered
HEEx, or nil to pass the name to the next provider. The
name is opaque: a provider parses whatever convention you adopt
("lucide-rocket", "fa-solid fa-x"). List several to
support several sets at once; the full context flows to each so a set can embed its
own sizing / hover marker.
"success" always
resolves to the framework glyph, never a provider.
<.icon> — smart dispatcher
Resolves in order: a framework affordance ("success",
"chevron-down", …) → masked pa-icon--* span,
built-in and reserved; then the configured :icon_providers
list; then the built-in fallback — "hero-X" → inline SVG via
<.heroicon>, anything else → <i class>
(Font Awesome, Bootstrap Icons, Lucide-font, etc.).
Affordance branch
<.icon name="success" />
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_providers (Lucide demo)
Most projects standardize on one or more icon sets — a custom SVG sprite
folder, a special font, a base64 sprite map, etc. Wire an ordered
:icon_providers list in config.exs;
<.icon> resolves framework affordances
(pa-icon--*) first, built-in, then hands every other name to
each provider in turn — the first to return markup wins.
1. Configure the provider list
# config/config.exs
config :keen_pure_admin,
icon_providers: [{MyAppWeb.Icons, :render}]
2. Define the function component
Each provider receives the full assigns map: name,
class, color, size,
size_value (resolved from size or the
configured default), variant, fill,
stroke, title, aria_label,
is_interactive. Pattern-match on name to
handle your set, and return nil for anything else so the
next provider (or the built-in hero-/FA fallback) gets a turn.
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
# Return nil for anything else — the next provider (or the
# library's built-in hero-/FA fallback) then handles it.
def render(_assigns), do: nil
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 provider:
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. |