# `PhiaUi.Components.Table`
[🔗](https://github.com/charlenopires/PhiaUI/blob/v0.1.17/lib/phia_ui/components/data/table.ex#L1)

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 {: .tip}
>
> 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

| Function          | HTML element  | Purpose                                  |
|-------------------|---------------|------------------------------------------|
| `table/1`         | `div > table` | Scrollable outer container               |
| `table_header/1`  | `thead`       | Column header section                    |
| `table_body/1`    | `tbody`       | Data rows section; streams-compatible    |
| `table_footer/1`  | `tfoot`       | Totals / summary rows                    |
| `table_row/1`     | `tr`          | Individual row with optional selection   |
| `table_head/1`    | `th`          | Column header cell                       |
| `table_cell/1`    | `td`          | Data cell with consistent padding        |
| `table_caption/1` | `caption`     | Accessible screen-reader caption         |

## When to use Table vs DataGrid

| Scenario                                               | Use             |
|--------------------------------------------------------|-----------------|
| Static or read-only data display                       | `Table`         |
| Simple lists with LiveView Streams                     | `Table`         |
| Server-side sorting, pagination, row selection         | `DataGrid`      |
| Column visibility toggle, toolbar, bulk actions        | `DataGrid`      |

## 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

# `table`

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

* `inner_block` (required) - Table structure: `table_header/1`, `table_body/1`, `table_footer/1`, `table_caption/1`.

# `table_action_cell`

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`

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`

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`

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:
  - `:sm` — `px-3 py-1.5` (compact)
  - `:md` — `px-4 py-3` (default)
  - `:lg` — `px-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`

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`

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:
  - `:sm` — `h-9 px-3 text-xs` (compact)
  - `:md` — `h-11 px-4 text-xs` (default)
  - `:lg` — `h-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`

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

* `inner_block` (required) - Header rows — typically one `table_row/1` containing `table_head/1` cells.

# `table_row`

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

* `inner_block` (required) - `table_head/1` or `table_cell/1` children.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
