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 (see PureAdmin.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.

Reserved names: qualify external names (prefix them) so they don't collide with the bare affordance names — a bare "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-launch
chart-bar
briefcase
user-group
cog-6-tooth
user
arrow-right-on-rectangle
plus
user-plus
arrow-down-tray
pencil
trash
shopping-cart
bookmark
paint-brush
chart-bar-square
home
list-bullet
view-columns
pencil-square
lock-closed
bell
swatch
squares-2x2
book-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:

rocket
lucide-rocket
bell
lucide-bell
settings
lucide-settings
user
lucide-user
house
lucide-house
search
lucide-search
plus
lucide-plus
trash
lucide-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

ComponentWhen
<.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.

John Doe

Administrator

Settings

Press Ctrl+K to preview the palette.
Sidebar
Display
Profile Panel
Body text size. All elements scale proportionally.
Commands
🚀
Deploy to Environment
Deploy a branch to an environment
g d
👤
Assign to User
Assign an item to a team member
g a
🧭
Go to Page
Navigate to a page
g g
🎨
Switch Theme
Change the visual theme
g t
Search
📦
Products
Search products
:products
📋
Orders
Search orders
:orders
👥
Users
Search users
:users
🧾
Invoices
Search invoices
:invoices