Files
Htmx/docs/Components/Table.md
T
2026-05-05 23:55:26 +05:00

5.3 KiB
Raw Blame History

Table

A styled HTML table with a header row, data rows, and optional caption and footer. Use it when you have a list of items with multiple columns — user lists, order history, product inventories.


Quick example

new Table(
    headers: new[] { "Name", "Email", "Role" },
    rows:    users.Select(u => new[]
    {
        System.Web.HttpUtility.HtmlEncode(u.DisplayName ?? ""),
        System.Web.HttpUtility.HtmlEncode(u.Email),
        u.Role
    }))

All the options

public Table(
    IEnumerable<string> headers,
    IEnumerable<IEnumerable<string>> rows,
    string caption = "",
    string footer  = "")
Parameter What it does
headers Column heading strings.
rows Each inner collection is one table row. Each string in it is a cell. Cells are raw HTML.
caption Optional summary text displayed below the table.
footer Optional footer text that spans all columns.

HTML safety: Cell values are inserted as raw HTML. Always use System.Web.HttpUtility.HtmlEncode() on any user-supplied strings before passing them in.


Real-world examples

new Table(
    headers: new[] { "Product", "Price", "Stock" },
    rows: products.Select(p => new[]
    {
        System.Web.HttpUtility.HtmlEncode(p.Name),
        $"${p.Price:F2}",
        p.Stock.ToString()
    }),
    caption: $"Showing {products.Count} products",
    footer:  "Prices include VAT")

Status column with a Badge

Pre-render the badge to an HTML string and embed it in the cell:

string RenderBadge(string label, string variant = "default")
{
    var buf = new System.Buffers.ArrayBufferWriter<byte>();
    new Badge(label, variant).Render(new HtmxRenderContext(buf));
    return System.Text.Encoding.UTF8.GetString(buf.WrittenSpan);
}

new Table(
    headers: new[] { "Name", "Status" },
    rows: users.Select(u => new[]
    {
        System.Web.HttpUtility.HtmlEncode(u.DisplayName ?? ""),
        u.IsActive ? RenderBadge("Active", "default") : RenderBadge("Inactive", "secondary")
    }))

Row actions with HTMX edit button

string EditLink(string id) =>
    $"""<button hx-get="/users/{id}/edit" hx-target="#modal" class="text-sm underline">Edit</button>""";

new Table(
    headers: new[] { "Name", "" },
    rows: users.Select(u => new[]
    {
        System.Web.HttpUtility.HtmlEncode(u.DisplayName ?? ""),
        EditLink(u.Id!)
    }))

How it works

Table wraps a standard <table> in an overflow-auto container so it scrolls horizontally on small screens. Header cells use <th> and data cells use <td>. The caption is rendered inside a <caption> element below the table; the footer spans all columns in a <tfoot> row.


---

## Tips and tricks

- Cells are raw HTML — HTML-encode any user-supplied string values before inserting them to prevent XSS.
- Use `System.Web.HttpUtility.HtmlEncode(value)` or `System.Net.WebUtility.HtmlEncode(value)` for any untrusted data.
- Pair with `Pagination` below the table for large datasets.
- For sortable columns, replace header strings with anchor tags containing HTMX sort-request attributes.
- The `footer` string spans all columns — use it for totals, notes, or a "load more" link.
- The `footer` string spans all columns — use it for totals, notes, or a "load more" link.

---

## Complete page example

**`Templates/AdminUsersPage.htmx`**
```html
<div class="max-w-5xl mx-auto py-10">
  <div class="flex items-center justify-between mb-6">
  <h1 class="text-2xl font-bold">Users</h1>
  $$InviteBtn$$
  </div>
  $$UsersTable$$
</div>

Templates/AdminUsersPage.htmx.cs

namespace Htmx.ApiDemo.Templates;

public sealed class AdminUsersPage : AdminUsersPageBase
{
  private readonly IHtmxComponent _table;
  private readonly IHtmxComponent _invite;

  public AdminUsersPage(IEnumerable<ApplicationUser> users, int total)
  {
    _invite = new Components.Button(
      "Invite user",
      variant: "default",
      extraAttributes: """hx-get="/admin/users/invite" hx-target="#modal" hx-swap="innerHTML" """);

    var rows = users.Select(u => new[]
    {
      System.Net.WebUtility.HtmlEncode(u.DisplayName ?? ""),
      System.Net.WebUtility.HtmlEncode(u.Email),
      u.CreatedAt.ToString("yyyy-MM-dd"),
      $"""<button class="text-destructive text-xs" hx-delete="/admin/users/{u.Id}" hx-confirm="Delete this user?">Delete</button>""",
    });

    _table = new Components.Table(
      caption:  "All registered accounts",
      headers:  new[] { "Name", "Email", "Joined", "Actions" },
      rows:     rows,
      footer:   $"{total} users total");
  }

  protected override void RenderInviteBtn(HtmxRenderContext ctx)   => _invite.Render(ctx.Next());
  protected override void RenderUsersTable(HtmxRenderContext ctx)  => _table.Render(ctx.Next());
}

GET handler

[Handler]
[MapGet("/admin/users")]
public static partial class GetAdminUsersHandler
{
  public record Query();
  private static async Task<IResult> HandleAsync(
    Query _, HttpContext ctx, MongoDbService db, CancellationToken ct)
  {
    var users = await db.GetAllUsersAsync(ct);
    var list  = users.ToList();
    return await ctx.WriteHtmxPage(
      new AdminUsersPage(list, list.Count), title: "Admin  Users");
  }
}