PhiaUi.Components.Table (phia_ui v0.1.17)

Copy Markdown View Source

Composable table component following the shadcn/ui anatomy.

Provides 8 independent sub-components that compose the standard HTML table hierarchy. Unlike data_grid/1 (which adds sorting, pagination, and row selection), Table is a pure presentational primitive — use it when you need a well-styled, accessible table without the overhead of interactive state.

LiveView Streams compatibility

This component is designed to work seamlessly with LiveView Streams (stream/3, stream_insert/3). Use table_body/1 as the stream container:

<.table_body id="users" phx-update="stream">
  <.table_row :for={{dom_id, user} <- @streams.users} id={dom_id}>
    <.table_cell><%= user.name %></.table_cell>
  </.table_row>
</.table_body>

Always set an id on table_body/1 when using phx-update="stream" — LiveView requires a stable ID to track the container across patches. For large datasets combine with phx-viewport-top / phx-viewport-bottom for infinite scroll pagination.

Sub-components

FunctionHTML elementPurpose
table/1div > tableScrollable outer container
table_header/1theadColumn header section
table_body/1tbodyData rows section; streams-compatible
table_footer/1tfootTotals / summary rows
table_row/1trIndividual row with optional selection
table_head/1thColumn header cell
table_cell/1tdData cell with consistent padding
table_caption/1captionAccessible screen-reader caption

When to use Table vs DataGrid

ScenarioUse
Static or read-only data displayTable
Simple lists with LiveView StreamsTable
Server-side sorting, pagination, row selectionDataGrid
Column visibility toggle, toolbar, bulk actionsDataGrid

Basic example

<.table>
  <.table_caption>Monthly revenue by product</.table_caption>
  <.table_header>
    <.table_row>
      <.table_head>Product</.table_head>
      <.table_head>Revenue</.table_head>
      <.table_head>Growth</.table_head>
      <.table_head>Status</.table_head>
    </.table_row>
  </.table_header>
  <.table_body>
    <.table_row>
      <.table_cell>Starter Plan</.table_cell>
      <.table_cell>$12,340</.table_cell>
      <.table_cell>+8%</.table_cell>
      <.table_cell><.badge>Active</.badge></.table_cell>
    </.table_row>
    <.table_row selected={true}>
      <.table_cell>Pro Plan</.table_cell>
      <.table_cell>$48,200</.table_cell>
      <.table_cell>+15%</.table_cell>
      <.table_cell><.badge variant={:default}>Active</.badge></.table_cell>
    </.table_row>
  </.table_body>
  <.table_footer>
    <.table_row>
      <.table_cell class="font-medium">Total</.table_cell>
      <.table_cell class="font-medium">$60,540</.table_cell>
      <.table_cell></.table_cell>
      <.table_cell></.table_cell>
    </.table_row>
  </.table_footer>
</.table>

LiveView Streams example

defmodule MyAppWeb.ProductsLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, stream(socket, :products, Products.list())}
  end

  def handle_info({:product_updated, product}, socket) do
    # stream_insert/3 patches only the changed row — no full re-render
    {:noreply, stream_insert(socket, :products, product)}
  end

  def render(assigns) do
    ~H"""
    <.table>
      <.table_header>
        <.table_row>
          <.table_head>Name</.table_head>
          <.table_head>Price</.table_head>
          <.table_head>Stock</.table_head>
        </.table_row>
      </.table_header>
      <.table_body id="products" phx-update="stream">
        <.table_row :for={{dom_id, product} <- @streams.products} id={dom_id}>
          <.table_cell><%= product.name %></.table_cell>
          <.table_cell><%= product.price %></.table_cell>
          <.table_cell><%= product.stock %></.table_cell>
        </.table_row>
      </.table_body>
    </.table>
    """
  end
end

Summary

Functions

Renders a scrollable table container.

Renders a standardised right-aligned action cell for the last column.

Renders the <tbody> section. Supports LiveView Streams via phx-update.

Renders a <caption> element for accessible table labelling.

Renders a <td> data cell.

Renders the <tfoot> section for totals and summary rows.

Renders a <th> column header cell.

Renders the <thead> section.

Renders a <tr> table row.

Functions

table(assigns)

Renders a scrollable table container.

The outer <div> has relative w-full overflow-auto so that wide tables scroll horizontally without breaking the page layout. The <table> inside uses w-full caption-bottom text-sm — it fills the container width and places captions below the table.

Example

<.table class="rounded-md border">
  <.table_header>...</.table_header>
  <.table_body>...</.table_body>
</.table>

Attributes

  • class (:string) - Additional CSS classes for the outer wrapper div (e.g. rounded-md border). Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the outer <div> wrapper.

Slots

table_action_cell(assigns)

Renders a standardised right-aligned action cell for the last column.

Wraps its content in a right-justified flex row so multiple action buttons sit consistently spaced. Use this instead of a plain table_cell/1 for Edit / Delete / View button columns to ensure visual consistency.

Example

<.table_row :for={user <- @users}>
  <.table_cell>{user.name}</.table_cell>
  <.table_cell>{user.email}</.table_cell>
  <.table_action_cell>
    <.button variant={:ghost} size={:sm} phx-click="edit" phx-value-id={user.id}>Edit</.button>
    <.button variant={:ghost} size={:sm} phx-click="delete" phx-value-id={user.id}>Delete</.button>
  </.table_action_cell>
</.table_row>

Attributes

  • class (:string) - Additional CSS classes for the <td>. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to the <td> element.

Slots

  • inner_block (required) - Action buttons — typically button components with variant={:ghost} or variant={:outline}.

table_body(assigns)

Renders the <tbody> section. Supports LiveView Streams via phx-update.

The [&_tr:last-child]:border-0 rule removes the bottom border from the last row, preventing a double border at the boundary between the table body and the table footer (or the container edge when no footer is present).

When striped={true}, even rows receive bg-muted/30 via [&_tr:nth-child(even)]:bg-muted/30.

Static usage

<.table_body>
  <.table_row :for={row <- @rows}>
    <.table_cell><%= row.name %></.table_cell>
  </.table_row>
</.table_body>

Striped usage

<.table_body striped>
  <.table_row :for={row <- @rows}>
    <.table_cell><%= row.name %></.table_cell>
  </.table_row>
</.table_body>

Streams usage

<.table_body id="rows" phx-update="stream">
  <.table_row :for={{dom_id, row} <- @streams.rows} id={dom_id}>
    <.table_cell><%= row.name %></.table_cell>
  </.table_row>
</.table_body>

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.

  • striped (:boolean) - When true, even-numbered rows receive a subtle bg-muted/30 background. Defaults to false.

  • Global attributes are accepted. HTML attributes forwarded to <tbody>. When using LiveView Streams, pass phx-update="stream" and id="unique-id" here:

    <.table_body id="users" phx-update="stream">

Slots

  • inner_block (required) - Data rows — table_row/1 children (static or :for loop).

table_caption(assigns)

Renders a <caption> element for accessible table labelling.

The caption element is placed after the table content (caption-side: bottom via caption-bottom on the parent <table>). It is visible to all users by default — use it for data source attribution, last-updated timestamps, or brief table descriptions.

The <caption> element is the preferred way to label a table for accessibility because it is directly associated with the table in the accessibility tree, unlike an aria-label or heading placed outside.

Example

<.table>
  <.table_caption>Revenue data as of March 2026</.table_caption>
  ...
</.table>

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to <caption>.

Slots

  • inner_block (required) - Caption text describing the table's purpose for screen readers and sighted users.

table_cell(assigns)

Renders a <td> data cell.

The default px-4 py-3 padding is consistent with table_head/1 headers so columns align correctly. Use the :class attr to override padding or alignment for specific columns:

<%!-- Right-align a numeric column --%>
<.table_cell class="text-right font-mono tabular-nums">$1,234.56</.table_cell>

<%!-- Compact cell for icon-only action column --%>
<.table_cell size={:sm}>
  <.button variant={:ghost} size={:icon}><.icon name="edit" /></.button>
</.table_cell>

Attributes

  • class (:string) - Additional CSS classes for padding or alignment overrides. Defaults to nil.

  • size (:atom) - Padding preset:

    • :smpx-3 py-1.5 (compact)
    • :mdpx-4 py-3 (default)
    • :lgpx-4 py-4 (comfortable)

    Defaults to :md. Must be one of :sm, :md, or :lg.

  • Global attributes are accepted. HTML attributes forwarded to <td> (e.g. colspan, rowspan, data-*).

Slots

  • inner_block (required) - Cell content — plain text, formatted numbers, badges, buttons, avatars, etc.

table_footer(assigns)

Renders the <tfoot> section for totals and summary rows.

Uses bg-muted/50 font-medium to visually distinguish footer rows from regular data rows. A top border (border-t) separates it from the body.

Example

<.table_footer>
  <.table_row>
    <.table_cell>Total</.table_cell>
    <.table_cell>$60,540</.table_cell>
    <.table_cell></.table_cell>
  </.table_row>
</.table_footer>

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to <tfoot>.

Slots

  • inner_block (required) - Footer rows — typically totals, averages, or summary statistics.

table_head(assigns)

Renders a <th> column header cell.

Uses text-xs uppercase tracking-wider for an enterprise-style compact header aesthetic. Content is left-aligned by default (text-left).

Example

<.table_head class="text-right">Amount</.table_head>
<.table_head size={:sm}>Compact Header</.table_head>
<.table_head>Status</.table_head>

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.

  • size (:atom) - Height and padding preset:

    • :smh-9 px-3 text-xs (compact)
    • :mdh-11 px-4 text-xs (default)
    • :lgh-12 px-4 text-sm (comfortable)

    Defaults to :md. Must be one of :sm, :md, or :lg.

  • Global attributes are accepted. HTML attributes forwarded to <th>.

Slots

  • inner_block (required) - Header cell content — column name, sort button, or checkbox.

table_header(assigns)

Renders the <thead> section.

The [&_tr]:border-b rule adds a bottom border to all header rows, creating a visual separator between the header and the first data row.

Example

<.table_header>
  <.table_row>
    <.table_head>Name</.table_head>
    <.table_head>Email</.table_head>
  </.table_row>
</.table_header>

Attributes

  • class (:string) - Additional CSS classes. Defaults to nil.
  • Global attributes are accepted. HTML attributes forwarded to <thead>.

Slots

table_row(assigns)

Renders a <tr> table row.

All rows have:

  • border-b — bottom divider (removed on last child by the tbody rule)
  • transition-colors — smooth background transition on hover/selection
  • hover:bg-muted/50 — subtle hover highlight

When selected={true}, the row gets data-state="selected" which activates the data-[state=selected]:bg-muted style.

Example

<.table_row selected={user.id in @selected_ids}>
  <.table_cell><%= user.name %></.table_cell>
</.table_row>

Attributes

  • class (:string) - Additional CSS classes for conditional row styling. Defaults to nil.

  • selected (:boolean) - When true, sets data-state="selected" on the <tr>, which triggers the data-[state=selected]:bg-muted Tailwind rule to highlight the row. Use this for externally managed selection state (e.g. from a form).

    Defaults to false.

  • Global attributes are accepted. HTML attributes forwarded to <tr>. When using streams, pass id={dom_id} so LiveView can track the element across patches.

Slots