Icons
Copy MarkdownPUI 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 |
<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.PUIIconsProviders 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
endThe 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.