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:
| Token | Use |
|---|---|
:close |
Close and dismiss controls |
:chevron_down |
Disclosure and downward navigation |
:chevron_left |
Previous navigation |
:chevron_right |
Next navigation |
:chevron_up_down |
Select trigger indicator |
:search |
Search controls |
:calendar |
Date picker trigger |
:menu |
Layout/sidebar controls |
:success, :error, :warning, :info |
Feedback status icons |
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.
The published Icons guide contains the same package documentation for consumers who are not using the demo application.