Current section
Files
Jump to
Current section
Files
lib/phia_ui/components/display/icon.ex
defmodule PhiaUi.Components.Icon do
@moduledoc """
Inline SVG icon component backed by a Lucide icon sprite file.
Icons are rendered via an SVG `<use>` element that references a symbol
in the `/icons/lucide-sprite.svg` sprite. Using a sprite avoids a separate
HTTP request per icon and allows the browser to cache the entire icon set
in a single file.
## Setup
Generate the sprite file by running the Mix task:
mix phia.icons
The generated file is placed at `priv/static/icons/lucide-sprite.svg` and
served by Phoenix's static plug at the public path `/icons/lucide-sprite.svg`.
## Sizes
| Atom | Tailwind classes | Pixel size |
|--------|-----------------|-----------|
| `:xs` | `w-3 h-3` | 12 px |
| `:sm` | `w-4 h-4` | 16 px |
| `:md` | `w-5 h-5` | 20 px |
| `:lg` | `w-6 h-6` | 24 px |
## Basic usage
<.icon name="menu" />
<.icon name="chevron-down" size={:sm} />
## Coloured icons
Icons inherit `currentColor` from the parent element's `color` / Tailwind
`text-*` classes:
<.icon name="trending-up" class="text-green-500" />
<.icon name="alert-circle" class="text-destructive" />
## Icon-only button (accessible)
When an icon is the sole content of a button, provide an `aria-label` on
the button so screen readers can announce the action:
<.button size={:icon} aria-label="Add item">
<.icon name="plus" />
</.button>
## Icon in a menu item
<.dropdown_menu_item>
<.icon name="settings" size={:sm} class="mr-2" />
Settings
</.dropdown_menu_item>
## Icon in a badge (trend indicator)
<.badge variant={:default} class="gap-1">
<.icon name="trending-up" size={:xs} />
+12.5%
</.badge>
## Icon with label in a navigation link
<.navigation_menu_link href="/analytics">
<.icon name="bar-chart-2" size={:sm} class="mr-2" />
Analytics
</.navigation_menu_link>
## Full icon reference
See the Lucide icon library at https://lucide.dev/icons for all available
icon names. Icons are referenced by their kebab-case name (e.g. `"arrow-up-right"`).
## Accessibility
The `<svg>` element is marked `aria-hidden="true"` and `focusable="false"`:
- `aria-hidden="true"` — removes the icon from the accessibility tree so
screen readers do not announce it separately.
- `focusable="false"` — prevents the SVG from receiving focus in older IE/Edge.
Always provide a text label or `aria-label` on the containing interactive
element when the icon is the only visible content.
"""
use Phoenix.Component
import PhiaUi.ClassMerger, only: [cn: 1]
attr(:name, :string,
required: true,
doc:
"Lucide icon name in kebab-case (e.g. `\"menu\"`, `\"chevron-down\"`, `\"trending-up\"`). See https://lucide.dev/icons for all available names."
)
attr(:size, :atom,
values: [:xs, :sm, :md, :lg],
default: :md,
doc:
"Icon size atom. Maps to Tailwind classes: `:xs` → `w-3 h-3`, `:sm` → `w-4 h-4`, `:md` → `w-5 h-5`, `:lg` → `w-6 h-6`."
)
attr(:class, :string,
default: nil,
doc:
"Additional CSS classes merged via `cn/1` (last wins). Use `text-*` for colour, `mr-N` / `ml-N` for spacing in flex containers."
)
@doc """
Renders an inline SVG icon from the Lucide sprite file.
The `<svg>` element is `aria-hidden="true"` so assistive technologies skip
it. When the icon is the only content of an interactive element (button,
link), add an `aria-label` to that element instead.
Icons inherit `currentColor` so they automatically match surrounding text
colour and respond to Tailwind `text-*` utilities.
## Example
<%!-- Default medium size --%>
<.icon name="save" />
<%!-- Small, coloured, with spacing --%>
<.icon name="check" size={:sm} class="text-green-500 mr-1" />
"""
def icon(assigns) do
~H"""
<svg
class={cn([size_class(@size), @class])}
aria-hidden="true"
focusable="false"
>
<%!-- `#icon-{name}` references the symbol ID generated by `mix phia.icons` --%>
<use href={"/icons/lucide-sprite.svg#icon-#{@name}"} />
</svg>
"""
end
# ---------------------------------------------------------------------------
# Private class helpers — all via pattern matching, never case/cond
# ---------------------------------------------------------------------------
# Each atom maps to a specific Tailwind size pair.
# Using pattern-matched defp heads avoids runtime conditionals and
# makes the mapping immediately visible for maintenance.
defp size_class(:xs), do: "w-3 h-3"
defp size_class(:sm), do: "w-4 h-4"
defp size_class(:md), do: "w-5 h-5"
defp size_class(:lg), do: "w-6 h-6"
end