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, direction
  • PhiaUi.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.Utilities

Table of Contents

Visual Identity

Feed & Timeline

Theme Controls

Utilities


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" />

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