Getting Started Icons

Icons

Provider-agnostic semantic icons with Heroicons, Lucide, Tabler, and custom markup integration.

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
Default semantic provider
Provider-rendered status
Application-owned icon slot
The host application can render any icon library through a slot.

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.