27 display components — icons, badges, avatars, activity feeds, timelines, chat UI, theme controls, and display utilities. These are the atoms of your UI.
Modules:
PhiaUi.Components.Display— icon, badge, avatar, avatar_group, activity_feed, timeline, chat_message, dark_mode_toggle, theme_provider, kbd, directionPhiaUi.Components.Utilities— visually_hidden, line_clamp, highlight_text, relative_time, number_format, reading_time, label_with_tooltip, print_only, screen_only, color_mode_value, diff_display, stat_unit, word_count, focus_trap, sticky_wrapper
import PhiaUi.Components.Display
import PhiaUi.Components.UtilitiesTable of Contents
Visual Identity
Feed & Timeline
Theme Controls
Utilities
- visually_hidden
- line_clamp
- highlight_text
- relative_time
- number_format
- reading_time
- label_with_tooltip
- print_only / screen_only
- color_mode_value
- diff_display
- stat_unit
- word_count
- focus_trap
- sticky_wrapper
icon
Lucide SVG sprite icon. Generate the sprite with mix phia.icons.
Sizes: :xs (12px) · :sm (16px) · :md (20px, default) · :lg (24px)
<.icon name="check" />
<.icon name="alert-triangle" size="lg" class="text-destructive" />
<.icon name="loader" class="animate-spin text-primary" />
<.icon name="arrow-up-right" size="xs" class="text-green-500" />Common names: home, settings, user, log-out, search, plus, trash, pencil, check, x, chevron-right, chevron-down, bar-chart-2, file-text, bell, star, shield, lock, mail, calendar, clock, upload, download
badge
Inline status and category labels.
Variants: default · secondary · destructive · outline
<.badge>Active</.badge>
<.badge variant="secondary">Draft</.badge>
<.badge variant="destructive">Failed</.badge>
<.badge variant="outline">Archived</.badge># Dynamic variant from Elixir
defp status_variant("active"), do: "default"
defp status_variant("draft"), do: "secondary"
defp status_variant("failed"), do: "destructive"
defp status_variant(_), do: "outline"<.badge variant={status_variant(@record.status)}>
<%= String.capitalize(@record.status) %>
</.badge>avatar
Circular profile image with initials fallback.
Sizes: :xs · :sm · :md (default) · :lg · :xl
<.avatar size="md">
<.avatar_image src={@user.avatar_url} alt={@user.name} />
<.avatar_fallback name={@user.name} />
</.avatar>
<%!-- Always-initials fallback --%>
<.avatar size="lg">
<.avatar_fallback name="Jane Doe" class="bg-primary text-primary-foreground" />
</.avatar>avatar_group
Stacked overlapping avatars with overflow count.
<.avatar_group max={4}>
<%= for member <- @team.members do %>
<.avatar size="sm">
<.avatar_image src={member.avatar_url} alt={member.name} />
<.avatar_fallback name={member.name} />
</.avatar>
<% end %>
</.avatar_group>Attrs: max (integer — show up to N, then +N more), class
kbd
Keyboard key indicator.
<span class="text-sm text-muted-foreground">
Press <.kbd>⌘</.kbd><.kbd>K</.kbd> to open the command palette
</span>Attrs: size (:sm | :md | :lg, default :md), class
activity_feed
Chronological activity list with icons and timestamps.
<.activity_feed>
<%= for event <- @events do %>
<.activity_feed_item
icon={event.icon}
title={event.title}
description={event.description}
timestamp={event.inserted_at}
/>
<% end %>
</.activity_feed>timeline
Vertical timeline with connector lines.
<.timeline>
<.timeline_item
title="Order placed"
description="Your order #1234 was confirmed."
timestamp={~N[2025-03-01 09:00:00]}
icon="package"
status={:complete}
/>
<.timeline_item
title="Delivery"
description="Estimated arrival March 5."
icon="home"
status={:pending}
/>
</.timeline>Statuses: :complete · :active · :pending
chat_message
A single chat bubble. Supports both sent and received sides, avatars, and timestamps.
<%= for msg <- @messages do %>
<.chat_message
content={msg.body}
side={if msg.user_id == @current_user.id, do: :right, else: :left}
avatar_src={msg.user.avatar_url}
name={msg.user.name}
timestamp={msg.inserted_at}
/>
<% end %>Attrs: side (:left | :right), content, avatar_src, name, timestamp
dark_mode_toggle
Toggles .dark on <html> and persists to localStorage.
<.dark_mode_toggle />
<.dark_mode_toggle show_label={true} />theme_provider
Sets a colour theme on its wrapper div via data-phia-theme.
<.theme_provider theme="violet">
<.button>Violet button</.button>
</.theme_provider>Available themes: zinc · slate · stone · gray · red · rose · orange · blue · green · violet
direction
RTL/LTR wrapper.
<.direction dir={:rtl}>
<p>مرحباً بالعالم</p>
</.direction>visually_hidden
Hides content visually but keeps it accessible to screen readers.
<button phx-click="close">
<.icon name="x" />
<.visually_hidden>Close dialog</.visually_hidden>
</button>Attrs: as (HTML tag string, default "span"), class
line_clamp
Truncates text to N lines with a client-side "Read more / Read less" toggle. Zero LiveView round-trips.
<.line_clamp id="post-body" lines={3}>
<%= @post.body %>
</.line_clamp>Attrs: id (required), lines (integer), class
highlight_text
XSS-safe text with query matches wrapped in <mark>. Case-insensitive.
<%!-- In a search results list --%>
<.highlight_text text={result.title} query={@search_query} />
<%!-- Custom mark style --%>
<.highlight_text
text={result.body}
query={@query}
mark_class="bg-yellow-200 dark:bg-yellow-800 rounded px-0.5"
/>Attrs: text, query, mark_class, class
relative_time
Renders "just now", "5 minutes ago", "3 days ago" etc. from a DateTime.
<.relative_time datetime={@post.inserted_at} />
<.relative_time datetime={@comment.inserted_at} now={@current_time} />Attrs: datetime (DateTime), now (DateTime, default DateTime.utc_now()), class
number_format
Formats numbers with compact notation, decimals, prefix, and suffix — server-side.
<%!-- "1.2M" --%>
<.number_format value={1_234_567} compact={true} />
<%!-- "$1,234.56" --%>
<.number_format value={1234.56} decimals={2} prefix="$" />
<%!-- "98.6%" --%>
<.number_format value={98.6} decimals={1} suffix="%" />Attrs: value (number), compact (boolean), decimals (integer), prefix, suffix
reading_time
Estimates read time from word count.
<.reading_time text={@post.body} />
<%!-- "4 min read" --%>
<.reading_time text={@post.body} wpm={200} />Attrs: text, wpm (integer, default 238), label (default "min read")
label_with_tooltip
A <label> with an inline help icon and tooltip.
<.label_with_tooltip
label="API Key"
tooltip="Your secret key. Never share it publicly."
for="api-key-input"
/>
<.input id="api-key-input" name="api_key" type="password" />print_only / screen_only
Visibility wrappers for print vs screen media.
<.print_only>
<p>Printed on <%= Date.utc_today() %></p>
</.print_only>
<.screen_only>
<.button>Export PDF</.button>
</.screen_only>color_mode_value
Renders different content in light vs dark mode via CSS.
<.color_mode_value>
<:light><img src="/logo-light.svg" alt="PhiaUI" /></:light>
<:dark><img src="/logo-dark.svg" alt="PhiaUI" /></:dark>
</.color_mode_value>diff_display
Word-level diff between two strings. Renders <del> (red) and <ins> (green) inline.
<.diff_display
before="The quick brown fox jumps"
after="The slow green fox leaps"
/>stat_unit
A value + unit pair with consistent styling. Common in dashboards.
<.stat_unit value="42" unit="req/s" />
<.stat_unit value="99.9" unit="% uptime" />
<.stat_unit value="1.2M" unit="users" />word_count
Counts words and displays the result.
<.word_count text="one two three" />
<%!-- "3 words" --%>
<.word_count text={@draft} class="text-xs text-muted-foreground" />focus_trap
Traps keyboard focus within a container for accessible modals. Hook: PhiaFocusTrap.
<.focus_trap id="modal-trap" enabled={@modal_open}>
<div role="dialog" aria-modal="true">
<h2>Modal title</h2>
<.button phx-click="close_modal">Close</.button>
</div>
</.focus_trap>Attrs: id (required), enabled (boolean, default true)
sticky_wrapper
Sticky positioning with configurable top offset.
<.sticky_wrapper offset_top={64}>
<div class="bg-card border-b px-6 py-3 flex items-center justify-between">
<span>3 rows selected</span>
<.button variant="destructive" size="sm">Delete selected</.button>
</div>
</.sticky_wrapper>Attrs: offset_top (integer px, default 0), class