Table

The table component is used to generate tables from arrays. Tables can be static or include features like filtering, sorting, pagination, reordering and others.

Example

<table
  x-data="table"
  :data-items="$store.table.data"
  :data-primary-key="$store.table.primaryKey"
  class="w-full table-auto border-collapse text-left text-[0.9rem]"
  class-loading="opacity-50 pointer-events-none"
>
  <thead>
    <tr>
      <template x-for="col in definition">
        <td
          x-bind="header"
          class="border-b border-gray-300 px-2 py-2 font-semibold text-text-800 dark:border-dark-600 dark:text-text-300"
        >
          <div class="flex items-center">
            <span x-text="col.label"></span>
            <template x-if="isSortable() && !isSorted()">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 320 512"
                fill="currentColor"
                class="ml-3 h-4 w-4 opacity-30"
              >
                <path
                  d="M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8L32 224c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z"
                />
              </svg>
            </template>
            <template x-if="isSortedAsc()">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 320 512"
                fill="currentColor"
                class="ml-3 h-4 w-4 opacity-70"
              >
                <path
                  d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"
                />
              </svg>
            </template>
            <template x-if="isSortedDesc()">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 320 512"
                fill="currentColor"
                class="ml-3 h-4 w-4 opacity-70"
              >
                <path
                  d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"
                />
              </svg>
            </template>
          </div>
        </td>
      </template>
    </tr>
  </thead>
  <tbody>
    <template x-for="row in getDataPaginated" :key="row[primaryKey]">
      <tr>
        <template x-for="col in definition">
          <td
            x-text="getCellContent"
            class="border-t border-gray-300 px-2 py-2 text-text-800 transition-colors duration-200 dark:border-dark-600 dark:text-text-300"
          ></td>
        </template>
      </tr>
    </template>
  </tbody>
</table>
document.addEventListener('alpine:init', () => {
  Alpine.store('table', {
    data: [
      {"id":1,"first_name":"Anthony","last_name":"Linbohm","city":"Makui","department":"Business Development","title":"Quality Engineer"},
      {"id":2,"first_name":"Richard","last_name":"Moult","city":"Xihu","department":"Legal","title":"Budget/Accounting Analyst IV"},
      {"id":3,"first_name":"Chance","last_name":"Dallas","city":"Moncton","department":"Support","title":"Product Engineer"},
      {"id":4,"first_name":"Rozamond","last_name":"Abbatucci","city":"Chico","department":"Legal","title":"Software Consultant"},
      {"id":5,"first_name":"Ashely","last_name":"Petrozzi","city":"Lafia","department":"Services","title":"Staff Accountant III"},
      {"id":6,"first_name":"Bron","last_name":"Siuda","city":"Mora","department":"Accounting","title":"Marketing Manager"},
      {"id":7,"first_name":"Marena","last_name":"Geraldi","city":"Karanganyar","department":"Support","title":"Compensation Analyst"},
      {"id":8,"first_name":"Tomas","last_name":"Donneely","city":"Meirinhas","department":"Services","title":"Research Associate"},
    ],
    primaryKey: 'id',
  })
})

Usage

<script defer src="https://cdn.jsdelivr.net/npm/litewind-alpine@0.x.x/components/table/dist/cdn.min.js"></script>

The data for the component is provided by the table function in the x-data directive and the props in the data-* attributes. Due to shared state such as filter, pagination, and more, it's recommended to set up the data as a separate store and use the $store magic to bind props to the table.

Props

data-items
[] Array

Data to display in the table.


data-primary-key
empty string String

This prop should be the name of the property that is unique for every record.


data-definition
[] Array

Table definition is an optional array of objects that defines columns of the table. See the detailed explanation below.


data-filter
empty string String

String used to filter items.


data-page
1 Number

Current page number.


data-items-per-page
0 Number

Number of records (rows) on the single page. Setting it to the 0 disables pagination.


data-is-loading
false Boolean

If true, adds classes from the class-loading attribute to the table. This can be useful, for example, when loading table data.

Events

update:items-filtered
Array

Event dispatched after each filtering of the items. This can be useful, for example to update pagination component.

Definition

The table definition is an optional array of objects that defines table's columns. Each object must include a unique key property, along with a number of optional properties. The key determines which property of the record object is rendered in the column. If key is not found, an empty column is added.

If definition is not provided, the component generates one using the first record in the data array. All additional properties are then set to default values. While this is sufficient for simple tables, using features like sorting or filtering requires providing a custom definition array.

key
empty string String

The key defines which property of the record object will be rendered in the column.


sortable
false Boolean

Enables sorting of the column.


filterable
true Boolean

Enables filtering of the column.


label
undefined String

Sets header for this column. If not provided header is the same as key converted to Header Case.

Example

<table
  x-data="table"
  :data-items="$store.tableDefinition.data"
  :data-definition="$store.tableDefinition.definition"
  :data-primary-key="$store.tableDefinition.primaryKey"
  class="w-full table-auto border-collapse text-left text-[0.9rem]"
  class-loading="opacity-50 pointer-events-none"
>
  <thead>
    <tr>
      <template x-for="col in definition">
        <td
          x-bind="header"
          class="border-b border-gray-300 px-2 py-2 font-semibold text-text-800 dark:border-dark-600 dark:text-text-300"
        >
          <div class="flex items-center">
            <span x-text="col.label"></span>
            <template x-if="isSortable() && !isSorted()">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 320 512"
                fill="currentColor"
                class="ml-3 h-4 w-4 opacity-30"
              >
                <path
                  d="M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8L32 224c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z"
                />
              </svg>
            </template>
            <template x-if="isSortedAsc()">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 320 512"
                fill="currentColor"
                class="ml-3 h-4 w-4 opacity-70"
              >
                <path
                  d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"
                />
              </svg>
            </template>
            <template x-if="isSortedDesc()">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 320 512"
                fill="currentColor"
                class="ml-3 h-4 w-4 opacity-70"
              >
                <path
                  d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"
                />
              </svg>
            </template>
          </div>
        </td>
      </template>
    </tr>
  </thead>
  <tbody>
    <template x-for="row in getDataPaginated" :key="row[primaryKey]">
      <tr>
        <template x-for="col in definition">
          <td
            class="border-t border-gray-300 px-2 py-2 text-text-800 transition-colors duration-200 dark:border-dark-600 dark:text-text-300"
          >
            <span x-text="getCellContent"></span>
            <template x-if="col.key === 'status'">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                id="mdi-checkbox-outline"
                viewBox="0 0 24 24"
                fill="currentColor"
                class="mx-auto h-5 w-5 text-green-600"
              >
                <path d="M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M19,5V19H5V5H19M10,17L6,13L7.41,11.58L10,14.17L16.59,7.58L18,9" />
              </svg>
            </template>
            <template x-if="col.key === 'edit'">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                id="mdi-account-edit"
                viewBox="0 0 24 24"
                fill="currentColor"
                class="mx-auto h-5 w-5"
              >
                <path d="M21.7,13.35L20.7,14.35L18.65,12.3L19.65,11.3C19.86,11.09 20.21,11.09 20.42,11.3L21.7,12.58C21.91,12.79 21.91,13.14 21.7,13.35M12,18.94L18.06,12.88L20.11,14.93L14.06,21H12V18.94M12,14C7.58,14 4,15.79 4,18V20H10V18.11L14,14.11C13.34,14.03 12.67,14 12,14M12,4A4,4 0 0,0 8,8A4,4 0 0,0 12,12A4,4 0 0,0 16,8A4,4 0 0,0 12,4Z" />
              </svg>
            </template>
          </td>
        </template>
      </tr>
    </template>
  </tbody>
</table>
document.addEventListener('alpine:init', () => {
  Alpine.store('tableDefinition', {
    data: [
      {"id":1,"first_name":"Anthony","last_name":"Linbohm","city":"Makui","department":"Business Development","title":"Quality Engineer"},
      {"id":2,"first_name":"Richard","last_name":"Moult","city":"Xihu","department":"Legal","title":"Budget/Accounting Analyst IV"},
      {"id":3,"first_name":"Chance","last_name":"Dallas","city":"Moncton","department":"Support","title":"Product Engineer"},
      {"id":4,"first_name":"Rozamond","last_name":"Abbatucci","city":"Chico","department":"Legal","title":"Software Consultant"},
      {"id":5,"first_name":"Ashely","last_name":"Petrozzi","city":"Lafia","department":"Services","title":"Staff Accountant III"},
      {"id":6,"first_name":"Bron","last_name":"Siuda","city":"Mora","department":"Accounting","title":"Marketing Manager"},
      {"id":7,"first_name":"Marena","last_name":"Geraldi","city":"Karanganyar","department":"Support","title":"Compensation Analyst"},
      {"id":8,"first_name":"Tomas","last_name":"Donneely","city":"Meirinhas","department":"Services","title":"Research Associate"},
      {"id":9,"first_name":"Umberto","last_name":"Cohalan","city":"Cuamba","department":"Research and Development","title":"Chemical Engineer"},
      {"id":10,"first_name":"Nicola","last_name":"Flippelli","city":"Faqīrwāli","department":"Human Resources","title":"VP Quality Control"},
    ],
    primaryKey: 'id',
    definition: [
      {
        key: 'status',
        label: 'Status',
      },
      {
        key: 'first_name',
        label: 'First name',
        sortable: true,
      },
      {
        key: 'last_name',
        label: 'Last name',
        sortable: true,
      },
      {
        key: 'title',
        sortable: true,
      },
      {
        key: 'department',
        sortable: true,
      },
      {
        key: 'edit',
        label: 'Edit',
      }
    ]
  })
})

Pagination and filtering

To enable pagination, set the data-items-per-page to any number greater than 0. The current page can be controlled with the page prop. In the example below, the table's current page is managed by the pagination component.

Filtering is enabled by default for all columns. The table data is filtered based on the string provided in the data-filter prop. The update:items-filtered event is used to update the item count for the pagination component.

Example

<div class="flex flex-col gap-y-4">
  <div
    x-data="input"
    x-model="$store.tableFilterPagination.filter"
    class="flex flex-1 items-center rounded-sm border px-3 py-2 outline-hidden transition-shadow duration-200 focus-within:ring-3 focus:outline-hidden"
    class-default="border-gray-300 bg-white focus-within:border-gray-400 focus-within:ring-primary-200 dark:border-dark-600 dark:bg-dark-800 dark:text-text-300 dark:focus-within:ring-primary-300"
    class-valid="border-success-300 bg-white text-success-600 focus-within:ring-success-200 dark:border-success-400 dark:bg-dark-800 dark:text-success-600 dark:focus-within:ring-success-300"
    class-invalid="border-danger-300 bg-white text-danger-600 focus-within:ring-danger-200 dark:border-danger-400 dark:bg-dark-800 dark:text-danger-600 dark:focus-within:ring-danger-300"
  >
    <div data-icon class="mr-3 empty:hidden"></div>
    <div data-prepend class="mr-3 empty:hidden"></div>
    <div class="mr-3 flex flex-1 flex-wrap">
      <input
        x-bind="input"
        type="text"
        class="w-full min-w-0 flex-1 border-0 bg-transparent p-0 outline-hidden focus:min-w-[64px] focus:outline-hidden"
      />
    </div>
    <div data-append class="mr-3 empty:hidden"></div>
    <div class="flex items-center gap-x-2">
      <div x-bind="loader">
        <svg
          viewBox="25 25 50 50"
          fill="none"
          class="h-5 w-5 animate-spinner-rotate"
        >
          <circle
            cx="50"
            cy="50"
            r="20"
            stroke="currentColor"
            stroke-width="4"
            stroke-miterlimit="10"
            stroke-linecap="round"
            class="animate-spinner-dash"
          />
        </svg>
      </div>
      <button x-bind="clearButton" @click="clear()" class="flex items-center">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="16"
          height="16"
          fill="currentColor"
          class="h-5 w-5 opacity-70"
          viewBox="0 0 16 16"
        >
          <path
            d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"
          />
        </svg>
      </button>
    </div>
  </div>

  <table
    x-data="table"
    :data-items="$store.tableFilterPagination.data"
    :data-items-per-page="$store.tableFilterPagination.itemsPerPage"
    :data-primary-key="$store.tableFilterPagination.primaryKey"
    :data-filter="$store.tableFilterPagination.filter"
    :data-page="$store.tableFilterPagination.page"
    @update:items-filtered="() => $store.tableFilterPagination.filteredItemsCount = $event.detail.length"
    class="w-full table-auto border-collapse text-left text-[0.9rem]"
    class-loading="opacity-50 pointer-events-none"
  >
    <thead>
      <tr>
        <template x-for="col in definition">
          <td
            x-bind="header"
            class="border-b border-gray-300 px-2 py-2 font-semibold text-text-800 dark:border-dark-600 dark:text-text-300"
          >
            <div class="flex items-center">
              <span x-text="col.label"></span>
              <template x-if="isSortable() && !isSorted()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-30"
                >
                  <path
                    d="M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8L32 224c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z"
                  />
                </svg>
              </template>
              <template x-if="isSortedAsc()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-70"
                >
                  <path
                    d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"
                  />
                </svg>
              </template>
              <template x-if="isSortedDesc()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-70"
                >
                  <path
                    d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"
                  />
                </svg>
              </template>
            </div>
          </td>
        </template>
      </tr>
    </thead>
    <tbody>
      <template x-for="row in getDataPaginated" :key="row[primaryKey]">
        <tr>
          <template x-for="col in definition">
            <td
              x-text="getCellContent"
              class="border-t border-gray-300 px-2 py-2 text-text-800 transition-colors duration-200 dark:border-dark-600 dark:text-text-300"
            ></td>
          </template>
        </tr>
      </template>
    </tbody>
  </table>

  <nav
    x-data="pagination"
    x-model="$store.tableFilterPagination.page"
    :data-items-count="$store.tableFilterPagination.filteredItemsCount"
    :data-items-per-page="$store.tableFilterPagination.itemsPerPage"
    data-max-pages="7"
    class="ml-auto flex w-auto gap-x-2"
  >
    <a
      x-bind="prevButton"
      class="flex w-12 cursor-pointer flex-col items-center justify-center rounded-sm border border-gray-300 p-2 text-sm font-medium hover:bg-secondary-50 dark:border-dark-600 dark:text-text-300 dark:hover:bg-dark-700"
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="16"
        height="16"
        fill="currentColor"
        viewBox="0 0 16 16"
      >
        <path
          fill-rule="evenodd"
          d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0"
        />
      </svg>
    </a>
    <template x-for="page in getPages">
      <a
        x-bind="pageButton"
        x-text="page"
        class="z-10 flex h-10 w-12 flex-none cursor-pointer items-center justify-center rounded-sm border text-sm font-semibold transition-shadow"
        class-default="border-gray-300 hover:bg-secondary-50 dark:border-dark-600 dark:text-text-300 dark:hover:bg-dark-800"
        class-selected="border-primary-200 bg-primary-500 font-semibold text-white ring-2 ring-primary-200 hover:bg-primary-400 dark:border-primary-200 dark:bg-primary-500 dark:text-text-100 dark:ring-primary-200 dark:hover:bg-primary-400"
      >
      </a>
    </template>
    <a
      x-bind="nextButton"
      class="flex w-12 cursor-pointer flex-col items-center justify-center rounded-sm border border-gray-300 p-2 text-sm font-medium hover:bg-secondary-50 dark:border-dark-600 dark:text-text-300 dark:hover:bg-dark-700"
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="16"
        height="16"
        fill="currentColor"
        viewBox="0 0 16 16"
      >
        <path
          fill-rule="evenodd"
          d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708"
        />
      </svg>
    </a>
  </nav>
</div>
document.addEventListener('alpine:init', () => {
  Alpine.store('tableFilterPagination', {
    data: [
      {"id":1,"first_name":"Anthony","last_name":"Linbohm","city":"Makui","department":"Business Development","title":"Quality Engineer"},
      {"id":2,"first_name":"Richard","last_name":"Moult","city":"Xihu","department":"Legal","title":"Budget/Accounting Analyst IV"},
      {"id":3,"first_name":"Chance","last_name":"Dallas","city":"Moncton","department":"Support","title":"Product Engineer"},
      {"id":4,"first_name":"Rozamond","last_name":"Abbatucci","city":"Chico","department":"Legal","title":"Software Consultant"},
      {"id":5,"first_name":"Ashely","last_name":"Petrozzi","city":"Lafia","department":"Services","title":"Staff Accountant III"},
      {"id":6,"first_name":"Bron","last_name":"Siuda","city":"Mora","department":"Accounting","title":"Marketing Manager"},
      {"id":7,"first_name":"Marena","last_name":"Geraldi","city":"Karanganyar","department":"Support","title":"Compensation Analyst"},
      {"id":8,"first_name":"Tomas","last_name":"Donneely","city":"Meirinhas","department":"Services","title":"Research Associate"},
      {"id":9,"first_name":"Umberto","last_name":"Cohalan","city":"Cuamba","department":"Research and Development","title":"Chemical Engineer"},
      {"id":10,"first_name":"Nicola","last_name":"Flippelli","city":"Faqīrwāli","department":"Human Resources","title":"VP Quality Control"},
      {"id":11,"first_name":"Jemie","last_name":"McLafferty","city":"Lagoa de Albufeira","department":"Human Resources","title":"Junior Executive"},
      {"id":12,"first_name":"Glen","last_name":"Edinborough","city":"Chicago","department":"Accounting","title":"Associate Professor"},
      {"id":13,"first_name":"Malachi","last_name":"Broadbridge","city":"Az Zaytūnīyah","department":"Human Resources","title":"Paralegal"},
      {"id":14,"first_name":"Yale","last_name":"Milnes","city":"Shuangjie","department":"Accounting","title":"Account Representative III"},
      {"id":15,"first_name":"Galvin","last_name":"Morrill","city":"Gaocun","department":"Engineering","title":"Account Representative II"},
      {"id":16,"first_name":"Cesar","last_name":"Pinnegar","city":"Drahichyn","department":"Marketing","title":"VP Sales"},
      {"id":17,"first_name":"Harlan","last_name":"Aldin","city":"Hulan","department":"Legal","title":"Paralegal"},
      {"id":18,"first_name":"Thadeus","last_name":"Tressler","city":"Indaial","department":"Services","title":"Product Engineer"},
      {"id":19,"first_name":"Marjie","last_name":"Agiolfinger","city":"Tatarbunary","department":"Marketing","title":"Environmental Specialist"},
      {"id":20,"first_name":"Amie","last_name":"Dupoy","city":"Thị Trấn Mường Khến","department":"Accounting","title":"Legal Assistant"},
      {"id":21,"first_name":"Marylinda","last_name":"Kidson","city":"Ondoy","department":"Business Development","title":"Community Outreach Specialist"},
      {"id":22,"first_name":"Karlen","last_name":"Capun","city":"Buda-Kashalyova","department":"Engineering","title":"Assistant Media Planner"},
      {"id":23,"first_name":"Horatius","last_name":"Giovanizio","city":"Kham Sakae Saeng","department":"Marketing","title":"Director of Sales"},
      {"id":24,"first_name":"Eleni","last_name":"Tale","city":"Montpellier","department":"Engineering","title":"Tax Accountant"},
      {"id":25,"first_name":"Chester","last_name":"Theuss","city":"Panshan","department":"Legal","title":"Account Executive"},
      {"id":26,"first_name":"Morey","last_name":"Demangel","city":"Stoney Ground","department":"Support","title":"Human Resources Manager"},
      {"id":27,"first_name":"Tedda","last_name":"Rawlin","city":"Erfangping","department":"Sales","title":"Editor"},
      {"id":28,"first_name":"Rennie","last_name":"Finnan","city":"Tuusula","department":"Human Resources","title":"Paralegal"},
      {"id":29,"first_name":"Merry","last_name":"Wisedale","city":"Renxian","department":"Services","title":"Systems Administrator IV"},
      {"id":30,"first_name":"Melodie","last_name":"Hayzer","city":"Hưng Nguyên","department":"Support","title":"Media Manager I"},
    ],
    filter: '',
    page: 1,
    itemsPerPage: 10,
    primaryKey: 'id',
    filteredItemsCount: 0,
  })
})

Table loading

The data-is-loading prop can be used, for example, when loading table data. Based on its value, the classes from the class-loading attribute are added to or removed from the table. By default, the opacity-50 and pointer-events-none classes are applied.

Example

<div x-data="{ isLoadingTable: false }">
  <button
    x-data
    @click="() => {
      isLoadingTable = true
      setTimeout(() => {
        isLoadingTable = false
      }, 3000)
    }"
    class="mx-auto block rounded-md border-violet-700 bg-violet-500 px-4 py-2 font-medium text-gray-100 hover:bg-violet-600 focus:ring-violet-200 dark:bg-violet-500 dark:hover:bg-violet-600 flex gap-x-2 items-center mb-4"
  >
    Refresh
    <svg viewBox="25 25 50 50" fill="none" class="animate-spinner-rotate h-4 w-4" x-show="isLoadingTable">
      <circle
        cx="50"
        cy="50"
        r="20"
        stroke="currentColor"
        stroke-width="4"
        stroke-miterlimit="10"
        stroke-linecap="round"
        class="animate-spinner-dash"
      />
    </svg>
  </button>
  <table
    x-data="table"
    :data-items="$store.table.data"
    :data-primary-key="$store.table.primaryKey"
    :data-is-loading="isLoadingTable"
    class="w-full table-auto border-collapse text-left text-[0.9rem]"
    class-loading="opacity-50 pointer-events-none"
  >
    <thead>
      <tr>
        <template x-for="col in definition">
          <td
            x-bind="header"
            class="border-b border-gray-300 px-2 py-2 font-semibold text-text-800 dark:border-dark-600 dark:text-text-300"
          >
            <div class="flex items-center">
              <span x-text="col.label"></span>
              <template x-if="isSortable() && !isSorted()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-30"
                >
                  <path
                    d="M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8L32 224c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z"
                  />
                </svg>
              </template>
              <template x-if="isSortedAsc()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-70"
                >
                  <path
                    d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"
                  />
                </svg>
              </template>
              <template x-if="isSortedDesc()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-70"
                >
                  <path
                    d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"
                  />
                </svg>
              </template>
            </div>
          </td>
        </template>
      </tr>
    </thead>
    <tbody>
      <template x-for="row in getDataPaginated" :key="row[primaryKey]">
        <tr>
          <template x-for="col in definition">
            <td
              x-text="getCellContent"
              class="border-t border-gray-300 px-2 py-2 text-text-800 transition-colors duration-200 dark:border-dark-600 dark:text-text-300"
            ></td>
          </template>
        </tr>
      </template>
    </tbody>
  </table>
</div>
document.addEventListener('alpine:init', () => {
  Alpine.store('table', {
    data: [
      {"id":1,"first_name":"Anthony","last_name":"Linbohm","city":"Makui","department":"Business Development","title":"Quality Engineer"},
      {"id":2,"first_name":"Richard","last_name":"Moult","city":"Xihu","department":"Legal","title":"Budget/Accounting Analyst IV"},
      {"id":3,"first_name":"Chance","last_name":"Dallas","city":"Moncton","department":"Support","title":"Product Engineer"},
      {"id":4,"first_name":"Rozamond","last_name":"Abbatucci","city":"Chico","department":"Legal","title":"Software Consultant"},
      {"id":5,"first_name":"Ashely","last_name":"Petrozzi","city":"Lafia","department":"Services","title":"Staff Accountant III"},
      {"id":6,"first_name":"Bron","last_name":"Siuda","city":"Mora","department":"Accounting","title":"Marketing Manager"},
      {"id":7,"first_name":"Marena","last_name":"Geraldi","city":"Karanganyar","department":"Support","title":"Compensation Analyst"},
      {"id":8,"first_name":"Tomas","last_name":"Donneely","city":"Meirinhas","department":"Services","title":"Research Associate"},
    ],
    primaryKey: 'id',
  })
})

Cell highlight

By default, the filtered portion of the cell content is not highlighted, as this requires the x-html directive. You can enable it by simply replacing x-text with x-html in the td element.

<td x-html="getHighlightedCellContent"></td>
Example

<div class="flex flex-col gap-y-4">
  <div
    x-data="input"
    x-model="$store.tableFilterPagination.filter"
    class="flex flex-1 items-center rounded-sm border px-3 py-2 outline-hidden transition-shadow duration-200 focus-within:ring-3 focus:outline-hidden"
    class-default="border-gray-300 bg-white focus-within:border-gray-400 focus-within:ring-primary-200 dark:border-dark-600 dark:bg-dark-800 dark:text-text-300 dark:focus-within:ring-primary-300"
    class-valid="border-success-300 bg-white text-success-600 focus-within:ring-success-200 dark:border-success-400 dark:bg-dark-800 dark:text-success-600 dark:focus-within:ring-success-300"
    class-invalid="border-danger-300 bg-white text-danger-600 focus-within:ring-danger-200 dark:border-danger-400 dark:bg-dark-800 dark:text-danger-600 dark:focus-within:ring-danger-300"
  >
    <div data-icon class="mr-3 empty:hidden"></div>
    <div data-prepend class="mr-3 empty:hidden"></div>
    <div class="mr-3 flex flex-1 flex-wrap">
      <input
        x-bind="input"
        type="text"
        class="w-full min-w-0 flex-1 border-0 bg-transparent p-0 outline-hidden focus:min-w-[64px] focus:outline-hidden"
      />
    </div>
    <div data-append class="mr-3 empty:hidden"></div>
    <div class="flex items-center gap-x-2">
      <div x-bind="loader">
        <svg
          viewBox="25 25 50 50"
          fill="none"
          class="h-5 w-5 animate-spinner-rotate"
        >
          <circle
            cx="50"
            cy="50"
            r="20"
            stroke="currentColor"
            stroke-width="4"
            stroke-miterlimit="10"
            stroke-linecap="round"
            class="animate-spinner-dash"
          />
        </svg>
      </div>
      <button x-bind="clearButton" @click="clear()" class="flex items-center">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          width="16"
          height="16"
          fill="currentColor"
          class="h-5 w-5 opacity-70"
          viewBox="0 0 16 16"
        >
          <path
            d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z"
          />
        </svg>
      </button>
    </div>
  </div>

  <table
    x-data="table"
    :data-items="$store.tableFilterPagination.data"
    :data-items-per-page="$store.tableFilterPagination.itemsPerPage"
    :data-primary-key="$store.tableFilterPagination.primaryKey"
    :data-filter="$store.tableFilterPagination.filter"
    :data-page="$store.tableFilterPagination.page"
    @update:items-filtered="() => $store.tableFilterPagination.filteredItemsCount = $event.detail.length"
    class="w-full table-auto border-collapse text-left text-[0.9rem]"
    class-loading="opacity-50 pointer-events-none"
  >
    <thead>
      <tr>
        <template x-for="col in definition">
          <td
            x-bind="header"
            class="border-b border-gray-300 px-2 py-2 font-semibold text-text-800 dark:border-dark-600 dark:text-text-300"
          >
            <div class="flex items-center">
              <span x-text="col.label"></span>
              <template x-if="isSortable() && !isSorted()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-30"
                >
                  <path
                    d="M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8L32 224c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8l256 0c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z"
                  />
                </svg>
              </template>
              <template x-if="isSortedAsc()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-70"
                >
                  <path
                    d="M182.6 137.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8l256 0c12.9 0 24.6-7.8 29.6-19.8s2.2-25.7-6.9-34.9l-128-128z"
                  />
                </svg>
              </template>
              <template x-if="isSortedDesc()">
                <svg
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 320 512"
                  fill="currentColor"
                  class="ml-3 h-4 w-4 opacity-70"
                >
                  <path
                    d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"
                  />
                </svg>
              </template>
            </div>
          </td>
        </template>
      </tr>
    </thead>
    <tbody>
      <template x-for="row in getDataPaginated" :key="row[primaryKey]">
        <tr>
          <template x-for="col in definition">
            <td
              x-html="getHighlightedCellContent"
              class="border-t border-gray-300 px-2 py-2 text-text-800 transition-colors duration-200 dark:border-dark-600 dark:text-text-300"
            ></td>
          </template>
        </tr>
      </template>
    </tbody>
  </table>

  <nav
    x-data="pagination"
    x-model="$store.tableFilterPagination.page"
    :data-items-count="$store.tableFilterPagination.filteredItemsCount"
    :data-items-per-page="$store.tableFilterPagination.itemsPerPage"
    data-max-pages="7"
    class="ml-auto flex w-auto gap-x-2"
  >
    <a
      x-bind="prevButton"
      class="flex w-12 cursor-pointer flex-col items-center justify-center rounded-sm border border-gray-300 p-2 text-sm font-medium hover:bg-secondary-50 dark:border-dark-600 dark:text-text-300 dark:hover:bg-dark-700"
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="16"
        height="16"
        fill="currentColor"
        viewBox="0 0 16 16"
      >
        <path
          fill-rule="evenodd"
          d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0"
        />
      </svg>
    </a>
    <template x-for="page in getPages">
      <a
        x-bind="pageButton"
        x-text="page"
        class="z-10 flex h-10 w-12 flex-none cursor-pointer items-center justify-center rounded-sm border text-sm font-semibold transition-shadow"
        class-default="border-gray-300 hover:bg-secondary-50 dark:border-dark-600 dark:text-text-300 dark:hover:bg-dark-800"
        class-selected="border-primary-200 bg-primary-500 font-semibold text-white ring-2 ring-primary-200 hover:bg-primary-400 dark:border-primary-200 dark:bg-primary-500 dark:text-text-100 dark:ring-primary-200 dark:hover:bg-primary-400"
      >
      </a>
    </template>
    <a
      x-bind="nextButton"
      class="flex w-12 cursor-pointer flex-col items-center justify-center rounded-sm border border-gray-300 p-2 text-sm font-medium hover:bg-secondary-50 dark:border-dark-600 dark:text-text-300 dark:hover:bg-dark-700"
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="16"
        height="16"
        fill="currentColor"
        viewBox="0 0 16 16"
      >
        <path
          fill-rule="evenodd"
          d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708"
        />
      </svg>
    </a>
  </nav>
</div>
document.addEventListener('alpine:init', () => {
  Alpine.store('tableFilterPagination', {
    data: [
      {"id":1,"first_name":"Anthony","last_name":"Linbohm","city":"Makui","department":"Business Development","title":"Quality Engineer"},
      {"id":2,"first_name":"Richard","last_name":"Moult","city":"Xihu","department":"Legal","title":"Budget/Accounting Analyst IV"},
      {"id":3,"first_name":"Chance","last_name":"Dallas","city":"Moncton","department":"Support","title":"Product Engineer"},
      {"id":4,"first_name":"Rozamond","last_name":"Abbatucci","city":"Chico","department":"Legal","title":"Software Consultant"},
      {"id":5,"first_name":"Ashely","last_name":"Petrozzi","city":"Lafia","department":"Services","title":"Staff Accountant III"},
      {"id":6,"first_name":"Bron","last_name":"Siuda","city":"Mora","department":"Accounting","title":"Marketing Manager"},
      {"id":7,"first_name":"Marena","last_name":"Geraldi","city":"Karanganyar","department":"Support","title":"Compensation Analyst"},
      {"id":8,"first_name":"Tomas","last_name":"Donneely","city":"Meirinhas","department":"Services","title":"Research Associate"},
      {"id":9,"first_name":"Umberto","last_name":"Cohalan","city":"Cuamba","department":"Research and Development","title":"Chemical Engineer"},
      {"id":10,"first_name":"Nicola","last_name":"Flippelli","city":"Faqīrwāli","department":"Human Resources","title":"VP Quality Control"},
      {"id":11,"first_name":"Jemie","last_name":"McLafferty","city":"Lagoa de Albufeira","department":"Human Resources","title":"Junior Executive"},
      {"id":12,"first_name":"Glen","last_name":"Edinborough","city":"Chicago","department":"Accounting","title":"Associate Professor"},
      {"id":13,"first_name":"Malachi","last_name":"Broadbridge","city":"Az Zaytūnīyah","department":"Human Resources","title":"Paralegal"},
      {"id":14,"first_name":"Yale","last_name":"Milnes","city":"Shuangjie","department":"Accounting","title":"Account Representative III"},
      {"id":15,"first_name":"Galvin","last_name":"Morrill","city":"Gaocun","department":"Engineering","title":"Account Representative II"},
      {"id":16,"first_name":"Cesar","last_name":"Pinnegar","city":"Drahichyn","department":"Marketing","title":"VP Sales"},
      {"id":17,"first_name":"Harlan","last_name":"Aldin","city":"Hulan","department":"Legal","title":"Paralegal"},
      {"id":18,"first_name":"Thadeus","last_name":"Tressler","city":"Indaial","department":"Services","title":"Product Engineer"},
      {"id":19,"first_name":"Marjie","last_name":"Agiolfinger","city":"Tatarbunary","department":"Marketing","title":"Environmental Specialist"},
      {"id":20,"first_name":"Amie","last_name":"Dupoy","city":"Thị Trấn Mường Khến","department":"Accounting","title":"Legal Assistant"},
      {"id":21,"first_name":"Marylinda","last_name":"Kidson","city":"Ondoy","department":"Business Development","title":"Community Outreach Specialist"},
      {"id":22,"first_name":"Karlen","last_name":"Capun","city":"Buda-Kashalyova","department":"Engineering","title":"Assistant Media Planner"},
      {"id":23,"first_name":"Horatius","last_name":"Giovanizio","city":"Kham Sakae Saeng","department":"Marketing","title":"Director of Sales"},
      {"id":24,"first_name":"Eleni","last_name":"Tale","city":"Montpellier","department":"Engineering","title":"Tax Accountant"},
      {"id":25,"first_name":"Chester","last_name":"Theuss","city":"Panshan","department":"Legal","title":"Account Executive"},
      {"id":26,"first_name":"Morey","last_name":"Demangel","city":"Stoney Ground","department":"Support","title":"Human Resources Manager"},
      {"id":27,"first_name":"Tedda","last_name":"Rawlin","city":"Erfangping","department":"Sales","title":"Editor"},
      {"id":28,"first_name":"Rennie","last_name":"Finnan","city":"Tuusula","department":"Human Resources","title":"Paralegal"},
      {"id":29,"first_name":"Merry","last_name":"Wisedale","city":"Renxian","department":"Services","title":"Systems Administrator IV"},
      {"id":30,"first_name":"Melodie","last_name":"Hayzer","city":"Hưng Nguyên","department":"Support","title":"Media Manager I"},
    ],
    filter: '',
    page: 1,
    itemsPerPage: 10,
    primaryKey: 'id',
    filteredItemsCount: 0,
  })
})
Litewind-alpine 0.1.0