Table

ui-table renders a native <table> from two properties: columns, which lists the keys and header labels, and rows, a list of plain objects. Each cell shows the row's value for the column key as text. The table fills the width of its container and scrolls horizontally when its content is wider.

Previewindex.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Table demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/table.html"></import-element>

  <ui-table
    caption="A list of your recent invoices."
    columns='[{"key":"invoice","label":"Invoice"},{"key":"status","label":"Status"},{"key":"method","label":"Method"},{"key":"amount","label":"Amount"}]'
    rows='[{"id":"INV001","invoice":"INV001","status":"Paid","method":"Credit card","amount":"$250.00"},{"id":"INV002","invoice":"INV002","status":"Pending","method":"PayPal","amount":"$150.00"},{"id":"INV003","invoice":"INV003","status":"Unpaid","method":"Bank transfer","amount":"$350.00"}]'>
  </ui-table>
</body>
</html>

Examples

Data from component state

In a component, bind columns and rows with _columns and _rows. Arrays and objects are passed as properties, so the table re-renders whenever the state changes. Here the rows are a getter that filters and sorts the data.

Previewindex.htmltable-invoices-demo.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Table demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/table.html"></import-element>

  <import-element from="./table-invoices-demo.html"></import-element>
  <table-invoices-demo></table-invoices-demo>
</body>
</html>
<custom-element name="table-invoices-demo">
  <script>
    const COLUMNS = [
      { key: "id", label: "Invoice" },
      { key: "customer", label: "Customer" },
      { key: "status", label: "Status" },
      { key: "total", label: "Amount" },
    ];
    const INVOICES = [
      { id: "INV001", customer: "Olivia Martin", status: "Paid", amount: 250 },
      { id: "INV002", customer: "Jackson Lee", status: "Pending", amount: 150 },
      { id: "INV003", customer: "Isabella Nguyen", status: "Unpaid", amount: 350 },
      { id: "INV004", customer: "William Kim", status: "Paid", amount: 450 },
      { id: "INV005", customer: "Sofia Davis", status: "Paid", amount: 550 },
    ];
    const currency = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });

    const invoices = useState({
      columns: COLUMNS,
      paidOnly: false,
      get rows() {
        return INVOICES.filter((invoice) => !this.paidOnly || invoice.status === "Paid")
          .toSorted((a, b) => b.amount - a.amount)
          .map((invoice) => ({ ...invoice, total: currency.format(invoice.amount) }));
      },
    });

    function filter(event) {
      invoices.paidOnly = event.target.checked;
    }
  </script>
  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 3);
      width: 100%;
    }
    label {
      display: flex;
      align-items: center;
      gap: calc(var(--spacing) * 2);
      font-size: var(--text-sm);
    }
  </style>
  <label><input type="checkbox" onchange="filter(event)"> Paid only</label>
  <ui-table _columns="#invoices.columns" _rows="#invoices.rows" caption="Sorted by amount, highest first."></ui-table>
</custom-element>

Column labels

label is optional. Without it, the header shows the column key.

Previewindex.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Table demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/table.html"></import-element>

  <ui-table
    columns='[{"key":"name"},{"key":"role"},{"key":"email","label":"Email address"}]'
    rows='[{"id":1,"name":"Ada","role":"Owner","email":"ada@example.com"},{"id":2,"name":"Linus","role":"Member","email":"linus@example.com"}]'>
  </ui-table>
</body>
</html>

Custom cells with a native table

ui-table cells hold text only. When cells need badges, buttons or links, write a native <table> in your component and repeat the rows with the repeat attribute on <tr> (inside <table>, <ul>, <ol> and <select>, use repeat on the item rather than <repeat-list>, so the HTML parser keeps the structure).

Previewindex.htmltable-members-demo.html
MemberRoleActions
Remove
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Table demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/avatar.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/badge.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <import-element from="./table-members-demo.html"></import-element>
  <table-members-demo></table-members-demo>
</body>
</html>
<custom-element name="table-members-demo">
  <script>
    const ROLE_VARIANTS = { Owner: "default", Admin: "secondary", Member: "outline" };

    const team = useState({
      members: [
        { id: 1, name: "Olivia Martin", email: "olivia@example.com", role: "Owner" },
        { id: 2, name: "Jackson Lee", email: "jackson@example.com", role: "Admin" },
        { id: 3, name: "Sofia Davis", email: "sofia@example.com", role: "Member" },
      ],
      get rows() {
        return this.members.map((member) => ({
          ...member,
          variant: ROLE_VARIANTS[member.role],
          initials: member.name.split(" ").map((part) => part[0]).join(""),
        }));
      },
    });

    function remove(member) {
      team.members = team.members.filter((item) => item.id !== member.id);
    }
  </script>
  <style>
    :host {
      display: block;
      width: 100%;
      overflow-x: auto;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      font-size: var(--text-sm);
    }
    th {
      height: calc(var(--spacing) * 10);
      padding: 0 calc(var(--spacing) * 2);
      text-align: left;
      font-weight: var(--font-weight-medium);
    }
    td {
      padding: calc(var(--spacing) * 2);
    }
    tr {
      border-bottom: 1px solid var(--border);
    }
    tbody tr:last-child {
      border-bottom: 0;
    }
    .person {
      display: flex;
      align-items: center;
      gap: calc(var(--spacing) * 3);
    }
    small {
      display: block;
      color: var(--muted-foreground);
    }
    .actions {
      text-align: right;
    }
  </style>
  <table>
    <thead>
      <tr><th>Member</th><th>Role</th><th class="actions">Actions</th></tr>
    </thead>
    <tbody>
      <tr repeat="#team.rows" as="member" key="id">
        <td>
          <div class="person">
            <ui-avatar><ui-avatar-fallback><slot name="#member.initials"></slot></ui-avatar-fallback></ui-avatar>
            <div><slot name="#member.name"></slot><small><slot name="#member.email"></slot></small></div>
          </div>
        </td>
        <td><ui-badge _variant="#member.variant"><slot name="#member.role"></slot></ui-badge></td>
        <td class="actions"><ui-button size="sm" variant="ghost" onclick="remove(member)">Remove</ui-button></td>
      </tr>
    </tbody>
  </table>
</custom-element>

API

Attributes

Attribute Type Default Description
columns array of { key: string, label?: string } [] The columns, in order. key is required and selects the value from each row. label is the header text and falls back to key. Accepts JSON in the attribute, or an array through the columns property or a _columns binding.
rows array of objects [] The rows. Each object's values are looked up by column key. Missing values render as empty cells. A row's id, when present, keys the row so updates reuse its DOM. Otherwise the index is used. Accepts JSON or an array, like columns.
caption string none Text for the table's <caption>, shown under the table. The caption is hidden when this is not set.

Properties

columns and rows are also properties. Assigning a new array re-renders the table:

<ui-table id="people"></ui-table>
<script>
  const table = document.getElementById("people");
  table.columns = [{ key: "name", label: "Name" }, { key: "email", label: "Email" }];
  table.rows = [{ id: 1, name: "Ada", email: "ada@example.com" }];
</script>

Parts

ui-table has no child parts and no slots. It renders everything from columns, rows and caption. For cells with markup, use a native table as shown above.

CSS parts

Part Description
table The native <table>.
head Each header cell (<th>).
row Each body row (<tr>).
cell Each body cell (<td>).

Rows get a bottom border and a muted background on hover. Cells do not wrap. Long tables scroll horizontally inside the element.

Accessibility

The output is a native <table> with <thead>, <th> header cells and an optional <caption>, so screen readers announce row and column headers. Set caption to give the table a name.

Edit this page on GitHub · View as Markdown