Skip to content

Table ​

Organizes data into rows and columns, with each row representing a single item and each column representing a specific attribute. Tables provide a structured way to display complex or tabular information clearly.

Usage ​

Use tables to present datasets, lists, or records where a clear relationship between rows and columns is important. Headers must be descriptive and for large datasets, consider features like sorting or filtering. Do NOT use tables for layout purposes.

API Reference ​

Component attribute(s) ​

x-h-table-container
x-h-table
x-h-table-header
x-h-table-head
x-h-table-cell
x-h-table-cell-button
x-h-table-body
x-h-table-group
x-h-table-group-row
x-h-table-group-button
x-h-table-row
x-h-table-caption
x-h-table-footer

Attributes ​

x-h-table-container ​

AttributeTypeRequiredDescription
data-borderbooleanfalseAdds a border to the table.

x-h-table ​

AttributeTypeRequiredDescription
data-bordersrows
columns
both
falseAdds borders between rows, columns or both.
data-fixedbooleanfalseFixed table layout. Incompatible with scroll mode.

x-h-table-header ​

AttributeTypeRequiredDescription
data-borderedtrue
horizontal
falseAdds a border around the header, or only a top and bottom border.

x-h-table-row ​

AttributeTypeRequiredDescription
data-stateselectedfalseSets a selected state to the row.
data-hoverablebooleanfalseMakes the row hoverable.
data-activablebooleanfalseMakes the row activable.

x-h-table-head ​

AttributeTypeRequiredDescription
data-hoverablebooleanfalseMakes the header cell hoverable.
data-activablebooleanfalseMakes the header cell activable.

x-h-table-cell ​

AttributeTypeRequiredDescription
data-hoverablebooleanfalseMakes the cell hoverable.
data-activablebooleanfalseMakes the cell activable.

x-h-table-group ​

AttributeTypeRequiredDescription
selfanyfalseExpression holding the collapsed state. A literal true only sets the initial state.

Modifiers ​

x-h-table-container ​

ModifierDescription
scrollAdds scroll ability to the container.

Data Slots ​

SlotElement
table-containerx-h-table-container
tablex-h-table
table-headerx-h-table-header
table-headx-h-table-head
table-cellx-h-table-cell
cell-input-buttonx-h-table-cell-button
table-bodyx-h-table-body
table-group-rowx-h-table-group-row
table-group-buttonx-h-table-group-button
table-rowx-h-table-row
table-captionx-h-table-caption
table-footerx-h-table-footer

Examples ​

Table with scroll ​

html
<div x-h-table-container.scroll data-border="true" style="max-height: 688px">
  <table x-h-table data-borders="both" x-data="tableData">
    <caption x-h-table-caption>
      Fruits & Vegetables
    </caption>
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Invoice Number</th>
        <th x-h-table-head scope="col" data-hoverable="true" data-activable="true">
          <div class="flex items-center justify-between gap-1">
            <span>Invoice Date</span>
            <svg x-h-lucide role="presentation" class="size-4" data-lucide="arrow-up-down"></svg>
          </div>
        </th>
        <th x-h-table-head scope="col">Customer Name</th>
        <th x-h-table-head scope="col">Due Date</th>
        <th x-h-table-head scope="col">Amount Total</th>
        <th x-h-table-head scope="col">Status</th>
        <th x-h-table-head scope="col">Outstanding Balance</th>
        <th x-h-table-head scope="col">Created By</th>
        <th x-h-table-head scope="col">Last Sent</th>
        <th x-h-table-head scope="col">Tax Amount</th>
        <th x-h-table-head scope="col">Category</th>
        <th x-h-table-head scope="col">Currency</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <template x-for="invoice in invoices">
        <tr x-h-table-row data-hoverable="true" data-activable="true">
          <th x-h-table-head x-text="invoice.invoiceNumber"></th>
          <td x-h-table-cell x-text="invoice.invoiceDate"></td>
          <td x-h-table-cell x-text="invoice.customerName"></td>
          <td x-h-table-cell x-text="invoice.dueDate"></td>
          <td x-h-table-cell x-text="invoice.amountTotal"></td>
          <td x-h-table-cell x-text="invoice.status"></td>
          <td x-h-table-cell x-text="invoice.outstandingBalance"></td>
          <td x-h-table-cell x-text="invoice.createdBy"></td>
          <td x-h-table-cell x-text="invoice.lastSent"></td>
          <td x-h-table-cell x-text="invoice.taxAmount"></td>
          <td x-h-table-cell x-text="invoice.category"></td>
          <td x-h-table-cell x-text="invoice.currency"></td>
        </tr>
      </template>
    </tbody>
    <tfoot x-h-table-footer>
      <tr x-h-table-row>
        <th x-h-table-head scope="row">Total Invoices</th>
        <td x-h-table-cell colspan="11" x-text="invoices.length"></td>
      </tr>
    </tfoot>
  </table>
</div>
<script type="text/javascript">
  function randomChoice(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
  }

  function randomFloat(min, max) {
    return Math.random() * (max - min) + min;
  }

  function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
  }

  function formatDate(date) {
    const day = date.getDate().toString().padStart(2, '0');
    const month = date.toLocaleString('en-US', { month: 'long' });
    const year = date.getFullYear();
    return `${day} ${month} ${year}`;
  }

  const names = ['Ivan Strashimechkarov', 'Anna Petrova', 'John Doe', 'Maria Gonzalez', 'Liam Smith', 'Emma Johnson', 'Noah Brown', 'Olivia Davis', 'William Wilson', 'Sophia Moore'];

  const categories = ['Consulting', 'Software', 'Design', 'Training', 'Marketing'];
  const statuses = ['paid', 'unpaid', 'overdue', 'partial'];

  function generateInvoices() {
    const baseDate = new Date();
    const invoices = [];
    for (let i = 0; i < 50; i++) {
      const invoiceDate = new Date(baseDate.getTime());
      invoiceDate.setDate(invoiceDate.getDate() + randomInt(0, 240));

      const dueDate = new Date(invoiceDate.getTime());
      dueDate.setDate(dueDate.getDate() + 20);

      const amountTotal = parseFloat(randomFloat(500, 5000).toFixed(2));
      const taxAmount = parseFloat((amountTotal * 0.1).toFixed(2));

      const status = randomChoice(statuses);

      let outstandingBalance;
      if (status === 'partial') {
        outstandingBalance = parseFloat((amountTotal * randomFloat(0.1, 0.65)).toFixed(2));
      } else if (status === 'paid') {
        outstandingBalance = 0.0;
      } else {
        outstandingBalance = amountTotal;
      }

      const invoice = {
        invoiceNumber: `INV-${String(i + 1).padStart(2, '0')}`,
        invoiceDate: formatDate(invoiceDate),
        customerName: randomChoice(names),
        dueDate: formatDate(dueDate),
        amountTotal: amountTotal,
        status: status,
        outstandingBalance: outstandingBalance,
        createdBy: 'admin',
        lastSent: formatDate(invoiceDate),
        taxAmount: taxAmount,
        category: randomChoice(categories),
        currency: 'EUR',
      };

      invoices.push(invoice);
    }
    return invoices;
  }
  Alpine.data('tableData', () => ({
    invoices: generateInvoices(),
  }));
</script>

Table with caption ​

html
<div x-h-table-container data-border="true">
  <table x-h-table data-borders="both" data-fixed="true">
    <caption x-h-table-caption>
      Fruits & Vegetables
    </caption>
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Item</th>
        <th x-h-table-head scope="col">Type</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <tr x-h-table-row>
        <td x-h-table-head>Cucumber</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Banana</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Asparagus</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row data-state="selected">
        <td x-h-table-head>Onion</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Apple</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
    </tbody>
  </table>
</div>

Table with row borders ​

html
<div x-h-table-container data-border="true">
  <table x-h-table data-borders="rows" data-fixed="true">
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Item</th>
        <th x-h-table-head scope="col">Type</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <tr x-h-table-row>
        <td x-h-table-head>Cucumber</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Banana</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Asparagus</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Onion</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Apple</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
    </tbody>
  </table>
</div>

Table with column borders ​

html
<div x-h-table-container data-border="true">
  <table x-h-table data-borders="columns" data-fixed="true">
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Item</th>
        <th x-h-table-head scope="col">Type</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <tr x-h-table-row>
        <td x-h-table-head>Cucumber</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Banana</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Asparagus</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Onion</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Apple</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
    </tbody>
  </table>
</div>

Table with no borders ​

html
<div x-h-table-container>
  <table x-h-table data-fixed="true">
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Item</th>
        <th x-h-table-head scope="col">Type</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <tr x-h-table-row>
        <td x-h-table-head>Cucumber</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Banana</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Asparagus</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Onion</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Apple</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
    </tbody>
  </table>
</div>

Table with bordered header ​

Set data-bordered="true" on the header to outline the whole head row, or data-bordered="horizontal" to draw only a top and bottom border. Use it on its own, not together with the table's other border options.

html
<div x-h-table-container>
  <table x-h-table data-fixed="true">
    <thead x-h-table-header data-bordered="true">
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Item</th>
        <th x-h-table-head scope="col">Type</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <tr x-h-table-row>
        <td x-h-table-head>Cucumber</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Banana</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Asparagus</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Onion</td>
        <td x-h-table-cell>Vegetable</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-head>Apple</td>
        <td x-h-table-cell>Fruit</td>
      </tr>
    </tbody>
  </table>
</div>

Table with inner borders and no container ​

html
<table x-h-table data-borders="both" data-fixed="true">
  <thead x-h-table-header>
    <tr x-h-table-row>
      <th x-h-table-head scope="col">Item</th>
      <th x-h-table-head scope="col">Type</th>
    </tr>
  </thead>
  <tbody x-h-table-body>
    <tr x-h-table-row>
      <td x-h-table-head>Cucumber</td>
      <td x-h-table-cell>Vegetable</td>
    </tr>
    <tr x-h-table-row>
      <td x-h-table-head>Banana</td>
      <td x-h-table-cell>Fruit</td>
    </tr>
    <tr x-h-table-row>
      <td x-h-table-head>Asparagus</td>
      <td x-h-table-cell>Vegetable</td>
    </tr>
    <tr x-h-table-row>
      <td x-h-table-head>Onion</td>
      <td x-h-table-cell>Vegetable</td>
    </tr>
    <tr x-h-table-row>
      <td x-h-table-head>Apple</td>
      <td x-h-table-cell>Fruit</td>
    </tr>
  </tbody>
</table>

Table with cell button ​

The cell button can be used to trigger some action.

html
<div x-h-table-container data-border="true">
  <table x-h-table data-borders="both" data-fixed="true">
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Header 1</th>
        <th x-h-table-head scope="col">Header 2</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <tr x-h-table-row>
        <td x-h-table-cell>Normal cell</td>
        <td x-h-table-cell>
          <button x-h-table-cell-button>Cell button</button>
        </td>
      </tr>
    </tbody>
  </table>
</div>

Table with inputs ​

You can use input, dropdown, select, date and time pickers inside a table by using the table modifier.

html
<div x-h-table-container.scroll data-border="true">
  <table x-h-table data-borders="both">
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Input</th>
        <th x-h-table-head scope="col">Dropdown</th>
        <th x-h-table-head scope="col">Select</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <tr x-h-table-row>
        <td x-h-table-cell>
          <input x-h-input.table placeholder="Name" />
        </td>
        <td x-h-table-cell>
          <button x-h-table-cell-button x-h-menu-trigger.dropdown>
            <span>Dropdown</span>
            <svg x-h-icon data-icon="chevron-down" role="presentation"></svg>
          </button>
          <ul x-h-menu aria-label="dropdown">
            <li x-h-menu-item>Item 1</li>
            <li x-h-menu-item>Item 2</li>
            <li x-h-menu-item>Item 3</li>
          </ul>
        </td>
        <td x-h-table-cell>
          <div x-h-select.table>
            <input x-h-select-input placeholder="Select" />
            <div x-h-select-content>
              <div x-h-select-list>
                <div x-h-select-option="'Option 1'" data-value="1"></div>
                <div x-h-select-option="'Option 2'" data-value="2"></div>
                <div x-h-select-option="'Option 3'" data-value="3"></div>
              </div>
            </div>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>
html
<div x-h-table-container.scroll data-border="true">
  <table x-h-table data-borders="both">
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Number</th>
        <th x-h-table-head scope="col">Date</th>
        <th x-h-table-head scope="col">Time</th>
      </tr>
    </thead>
    <tbody x-h-table-body>
      <tr x-h-table-row>
        <td x-h-table-cell>
          <div x-h-input-number.table>
            <input type="number" min="0" max="10" step="2" value="4" />
          </div>
        </td>
        <td x-h-table-cell>
          <div x-h-date-picker.table x-data="{ date: new Date().toISOString() }">
            <input type="text" id="tableDate" />
            <button x-h-date-picker-trigger aria-label="Choose date"></button>
            <div x-h-date-picker-popup x-model="date"></div>
          </div>
        </td>
        <td x-h-table-cell>
          <div x-data="{ timeConfig: { is12Hour: true } }" x-h-time-picker.table="timeConfig">
            <input type="text" id="tableTime" x-h-time-picker-input />
            <div x-h-time-picker-popup></div>
          </div>
        </td>
      </tr>
    </tbody>
  </table>
</div>

Grouped rows ​

Stack x-h-table-group on a x-h-table-body to turn that body into a collapsible group. Its first row is the group title. Give it x-h-table-group-row, a single td cell spanning all columns, and a x-h-table-group-button with the title as children (a <span> for example) rather than x-text, which would replace the collapse arrow. Set x-h-table-group="true" to start a group collapsed.

html
<div x-h-table-container data-border="true">
  <table x-h-table data-borders="rows">
    <thead x-h-table-header>
      <tr x-h-table-row>
        <th x-h-table-head scope="col">Product</th>
        <th x-h-table-head scope="col">Stock</th>
      </tr>
    </thead>
    <tbody x-h-table-body x-h-table-group>
      <tr x-h-table-group-row>
        <td x-h-table-cell colspan="2">
          <button x-h-table-group-button><span>Fruits</span> <span class="text-xs font-normal text-muted-foreground">2 items</span></button>
        </td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-cell>Apples</td>
        <td x-h-table-cell>140</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-cell>Oranges</td>
        <td x-h-table-cell>72</td>
      </tr>
    </tbody>
    <tbody x-h-table-body x-h-table-group="true">
      <tr x-h-table-group-row>
        <td x-h-table-cell colspan="2">
          <button x-h-table-group-button><span>Vegetables</span> <span class="text-xs font-normal text-muted-foreground">2 items</span></button>
        </td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-cell>Carrots</td>
        <td x-h-table-cell>56</td>
      </tr>
      <tr x-h-table-row>
        <td x-h-table-cell>Potatoes</td>
        <td x-h-table-cell>210</td>
      </tr>
    </tbody>
  </table>
</div>

Collapse all groups ​

Bind the same expression to every group to drive them together, for example from a toolbar button. Clicking a group's own button still toggles just that group.

html
<div x-data="{ allCollapsed: false }" class="vbox items-start gap-2">
  <button x-h-button @click="allCollapsed = !allCollapsed" x-text="allCollapsed ? 'Expand all' : 'Collapse all'"></button>
  <div x-h-table-container data-border="true">
    <table x-h-table data-borders="rows">
      <thead x-h-table-header>
        <tr x-h-table-row>
          <th x-h-table-head scope="col">Product</th>
          <th x-h-table-head scope="col">Stock</th>
        </tr>
      </thead>
      <tbody x-h-table-body x-h-table-group="allCollapsed">
        <tr x-h-table-group-row>
          <td x-h-table-cell colspan="2">
            <button x-h-table-group-button><span>Fruits</span></button>
          </td>
        </tr>
        <tr x-h-table-row>
          <td x-h-table-cell>Apples</td>
          <td x-h-table-cell>140</td>
        </tr>
      </tbody>
      <tbody x-h-table-body x-h-table-group="allCollapsed">
        <tr x-h-table-group-row>
          <td x-h-table-cell colspan="2">
            <button x-h-table-group-button><span>Vegetables</span></button>
          </td>
        </tr>
        <tr x-h-table-row>
          <td x-h-table-cell>Carrots</td>
          <td x-h-table-cell>56</td>
        </tr>
      </tbody>
    </table>
  </div>
</div>