Table

New

Overview

A responsive, stream-powered table with semantic headers, body rows, captions, and footers.

A list of recent invoices.
Invoice Status Method Amount
INV001 Paid Credit card 25000
INV002 Pending PayPal 15000
INV003 Unpaid Bank transfer 35000
INV004 Paid Credit card 45000
Total $1,200.00
HEEx
<Table.table container_class="rounded-xl border border-border">
  <caption>A list of recent invoices.</caption>
  <Table.table_head>
    <:col class="w-28">Invoice</:col>
    <:col>Status</:col>
    <:col>Method</:col>
    <:col class="text-right">Amount</:col>
  </Table.table_head>
  <Table.table_body id="invoices">
    <Table.table_row :for={{dom_id, invoice} <- @streams.invoices} id={dom_id}>
      <:cell class="font-medium">{invoice.number}</:cell>
      <:cell>{invoice.status}</:cell>
      <:cell>{invoice.method}</:cell>
      <:cell class="text-right">{invoice.amount}</:cell>
    </Table.table_row>
  </Table.table_body>
  <tfoot>
    <tr>
      <td colspan="3">Total</td>
      <td class="text-right">$1,200.00</td>
    </tr>
  </tfoot>
</Table.table>

Sortable headers

Put a keyboard-accessible button inside a sortable column header and set aria-sort on the currently sorted column. The parent LiveView handles the event and supplies the reordered stream.

Method
INV001 Paid Credit card $250.00
INV002 Pending PayPal $150.00
INV003 Unpaid Bank transfer $350.00
INV004 Paid Credit card $450.00
HEEx
<Table.table container_class="rounded-xl border border-border">
  <Table.table_head>
    <:col aria-sort={if @table_sort == {:number, :asc}, do: "ascending", else: if(@table_sort == {:number, :desc}, do: "descending", else: "none")}>
      <Button.button kind={:ghost} size={:sm} class="-mx-2" phx-click="sort-table" phx-value-column="number">
        Invoice <Icon.icon name={if @table_sort == {:number, :asc}, do: "hero-chevron-up", else: if(@table_sort == {:number, :desc}, do: "hero-chevron-down", else: "hero-chevron-up-down")} class="size-4" />
      </Button.button>
    </:col>
    <:col aria-sort={if @table_sort == {:status, :asc}, do: "ascending", else: if(@table_sort == {:status, :desc}, do: "descending", else: "none")}>
      <Button.button kind={:ghost} size={:sm} class="-mx-2" phx-click="sort-table" phx-value-column="status">
        Status <Icon.icon name={if @table_sort == {:status, :asc}, do: "hero-chevron-up", else: if(@table_sort == {:status, :desc}, do: "hero-chevron-down", else: "hero-chevron-up-down")} class="size-4" />
      </Button.button>
    </:col>
    <:col>Method</:col>
    <:col class="text-right" aria-sort={if @table_sort == {:amount, :asc}, do: "ascending", else: if(@table_sort == {:amount, :desc}, do: "descending", else: "none")}>
      <Button.button kind={:ghost} size={:sm} class="-mr-2 ml-auto" phx-click="sort-table" phx-value-column="amount">
        Amount <Icon.icon name={if @table_sort == {:amount, :asc}, do: "hero-chevron-up", else: if(@table_sort == {:amount, :desc}, do: "hero-chevron-down", else: "hero-chevron-up-down")} class="size-4" />
      </Button.button>
    </:col>
  </Table.table_head>
  <Table.table_body id="sortable-invoices">
    <Table.table_row :for={{dom_id, invoice} <- @streams.invoices} id={dom_id}>
      <:cell class="font-medium">{invoice.number}</:cell>
      <:cell>{invoice.status}</:cell>
      <:cell>{invoice.method}</:cell>
      <:cell class="text-right">{invoice.amount}</:cell>
    </Table.table_row>
  </Table.table_body>
</Table.table>

Stream containers

Supply a stable id to Table.table_body. It always renders phx-update="stream", and every row requires a stable ID. Use the DOM ID from each stream tuple for streamed rows.

Invoice Status Amount
INV001 Paid $250.00
INV002 Pending $150.00
INV003 Unpaid $350.00
INV004 Paid $450.00
HEEx
<Table.table container_class="rounded-xl border border-border">
  <Table.table_head>
    <:col>Invoice</:col>
    <:col>Status</:col>
    <:col class="text-right">Amount</:col>
  </Table.table_head>
  <Table.table_body id="invoices">
    <Table.table_row id="invoices-empty" class="hidden only:table-row">
      <:cell colspan="3" class="py-10 text-center text-muted-foreground">
        No invoices found.
      </:cell>
    </Table.table_row>
    <Table.table_row :for={{dom_id, invoice} <- @streams.invoices} id={dom_id}>
      <:cell class="font-medium">{invoice.number}</:cell>
      <:cell>{invoice.status}</:cell>
      <:cell class="text-right">{invoice.amount}</:cell>
    </Table.table_row>
  </Table.table_body>
</Table.table>

Rich rows

Cell slots accept arbitrary HEEx, native cell attributes, and per-column utility classes. Use them for stacked text, status badges, and row actions without changing the table API.

Product Status Price Actions
Wireless mouse
ELC-001
In stock $29.99
Mechanical keyboard
ELC-014
Low stock $129.99
USB-C hub
ACC-072
Out of stock $49.99
HEEx
<Table.table container_class="rounded-xl border border-border">
  <Table.table_head>
    <:col>Product</:col>
    <:col>Status</:col>
    <:col class="text-right">Price</:col>
    <:col><span class="sr-only">Actions</span></:col>
  </Table.table_head>
  <Table.table_body id="products">
    <Table.table_row :for={{dom_id, product} <- @streams.products} id={dom_id}>
      <:cell>
        <div class="font-medium">{product.name}</div>
        <div class="text-sm text-muted-foreground">{product.sku}</div>
      </:cell>
      <:cell><Badge.badge kind={product.badge}>{product.status}</Badge.badge></:cell>
      <:cell class="text-right font-mono">{product.price}</:cell>
      <:cell class="text-right">
        <Button.button kind={:ghost} size={:icon_sm} aria-label={"Actions for #{product.name}"}>
          <Icon.icon name="hero-ellipsis-horizontal" />
        </Button.button>
      </:cell>
    </Table.table_row>
  </Table.table_body>
</Table.table>

Add this module

terminal
mix dizzy.add table

Component API

Table.table

Attributes

Attributes for Table.table
NameTypeDefault Values
class :any nilโ€”
container_class :any nilโ€”
rest :global nilโ€”

Slots

Slots for Table.table
NameRequired
inner_block Required

Table.table_head

Attributes

Attributes for Table.table_head
NameTypeDefault Values
class :any nilโ€”
rest :global nilโ€”

Slots

Slots for Table.table_head
NameRequired
col Required

Table.table_body

Attributes

Attributes for Table.table_body
NameTypeDefault Values
class :any nilโ€”
id :string Requiredโ€”
rest :global nilโ€”

Slots

Slots for Table.table_body
NameRequired
inner_block Required

Table.table_row

Attributes

Attributes for Table.table_row
NameTypeDefault Values
class :any nilโ€”
id :string Requiredโ€”
rest :global nilโ€”

Slots

Slots for Table.table_row
NameRequired
cell Required

Explore Dizzy