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-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_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:

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

Sidebar
Display
Profile Panel
Body text size. All elements scale proportionally.