PUI separates icon intent from icon rendering. PUI-owned affordances use semantic tokens through PUI.Icon; application-owned icons are rendered by the host application and passed through component slots.

Default provider

PUI ships a dependency-free Heroicons provider that renders the CSS classes generated by Phoenix's default Heroicons integration:

<PUI.Icon.icon name={:calendar} />
<PUI.Icon.icon name={:close} class="size-5 text-muted-foreground" />

No configuration is required when the host application already includes the Phoenix Heroicons Tailwind plugin.

The semantic tokens currently owned by PUI are:

TokenUse
:closeClose and dismiss controls
:chevron_downDisclosure and downward navigation
:chevron_leftPrevious navigation
:chevron_rightNext navigation
:chevron_up_downSelect trigger indicator
:searchSearch controls
:calendarDate picker trigger
:menuLayout/sidebar controls
:success, :error, :warning, :infoFeedback status icons

<AppWeb.DocsDemo.icons_demo />

Configure a provider

Select one provider for all PUI internals in the host application's config:

config :pui, :icon_provider, MyApp.PUIIcons

Providers implement PUI.IconProvider:

defmodule MyApp.PUIIcons do
  use Phoenix.Component

  @behaviour PUI.IconProvider

  @mapping %{
    close: "provider-close",
    calendar: "provider-calendar",
    chevron_down: "provider-chevron-down",
    chevron_left: "provider-chevron-left",
    chevron_right: "provider-chevron-right",
    chevron_up_down: "provider-chevrons-up-down",
    search: "provider-search",
    menu: "provider-menu",
    success: "provider-success",
    error: "provider-error",
    warning: "provider-warning",
    info: "provider-info"
  }

  @impl true
  def render(token, assigns) do
    assigns = assign(assigns, :provider_name, Map.fetch!(@mapping, token))

    ~H"""
    <MyAppWeb.CoreComponents.icon
      name={@provider_name}
      class={@class}
      {@rest}
    />
    """
  end
end

The adapter may call an existing host icon component, render an inline SVG, or emit provider-specific classes. Unsupported semantic tokens should fail clearly rather than render an invisible control.

Lucide and Tabler

The provider module is intentionally independent of any icon package. A Lucide adapter maps the PUI tokens to the Lucide names used by the host:

@mapping %{
  close: "lucide-x",
  calendar: "lucide-calendar",
  chevron_down: "lucide-chevron-down",
  chevron_left: "lucide-chevron-left",
  chevron_right: "lucide-chevron-right",
  chevron_up_down: "lucide-chevrons-up-down",
  search: "lucide-search",
  menu: "lucide-menu",
  success: "lucide-circle-check",
  error: "lucide-circle-x",
  warning: "lucide-triangle-alert",
  info: "lucide-circle-help"
}

For Tabler, keep the same behaviour and change only the mapping and the host renderer:

@mapping %{
  close: "tabler-x",
  calendar: "tabler-calendar",
  chevron_down: "tabler-chevron-down",
  chevron_left: "tabler-chevron-left",
  chevron_right: "tabler-chevron-right",
  chevron_up_down: "tabler-selector",
  search: "tabler-search",
  menu: "tabler-menu-2",
  success: "tabler-circle-check",
  error: "tabler-circle-x",
  warning: "tabler-alert-triangle",
  info: "tabler-info-circle"
}

The exact provider names and Tailwind/SVG setup belong to the host application; PUI only relies on the semantic token contract.

Application-owned icons

Use slots when the icon represents application navigation, business meaning, or an icon outside PUI's small semantic vocabulary:

<.sidebar_menu_item title="Dashboard" href="/dashboard">
  <:icon>
    <.icon name="hero-home" class="size-4" />
  </:icon>
</.sidebar_menu_item>

The slot can contain a Phoenix Heroicon, a Lucide component, a Tabler component, or custom SVG markup. PUI owns the surrounding layout and accessibility relationship, while the host owns the icon library.