0 Tk

Table

Display data in a table format.

Usage

Use the rows prop to display data in a table. By default, all fields from the rows will be displayed.Props Prop Type Description checked Boolean Indicates if all rows are selected change Function Function to handle selection state changes. Must receive a boolean value (true/false) indeterminate Boolean Indicates partial selection (when some rows are selected)

<script setup lang="ts">
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}]
</script>

<template>
  <UTable :rows="people" />
</template>

Columns

Configure displayed columns using the columns prop. Each column object can have these properties:

  • label - Header label (configurable via column-attribute prop)

  • key - Field to display from row data

  • sortable - Enable column sorting (defaults to false)

  • direction - Initial sort direction (defaults to asc)

  • class - CSS class for column cells

  • rowClass - CSS class for data cells

  • sort - Custom sort function (defaults to simple comparison)

    Arguments for the sort function are: Value A, Value B, Direction - ‘asc’ or ‘desc’

    Example sort function:

    (a, b, direction) => {
      if (!a || !b) return 0
      const aPrice = parseInt(a.replace(/[,$]/g, ""))
      const bPrice = parseInt(b.replace(/[,$]/g, ""))
      return direction === "asc" ? aPrice - bPrice : bPrice - aPrice
    }
    
<script setup lang="ts">
const columns = [{
  key: 'id',
  label: 'ID'
}, {
  key: 'name',
  label: 'User name'
}, {
  key: 'title',
  label: 'Job position'
}, {
  key: 'email',
  label: 'Email'
}, {
  key: 'role',
  label: 'Role'
}]

const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}]
</script>

<template>
  <UTable :columns="columns" :rows="people" />
</template>

Column Selection

Use SelectMenu to allow users to choose visible columns:

<script setup lang="ts">
const selectedColumns = ref([...columns])
</script>

<template>
  <div>
    <div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
      <USelectMenu v-model="selectedColumns" :options="columns" multiple placeholder="Columns" />
    </div>

    <UTable :columns="selectedColumns" :rows="people" />
  </div>
</template>

Column Styling

Apply styles to table elements using class properties:

<script setup lang="ts">
const columns = [{
  key: 'id',
  label: '#'
}, {
  key: 'quantity',
  label: 'Quantity',
  class: 'italic'
}, {
  key: 'name',
  label: 'Name'
}]

const items = [{
  id: 1,
  name: 'Apple',
  quantity: { value: 100, class: 'bg-green-500/50 dark:bg-green-400/50' }
}, {
  id: 2,
  name: 'Orange',
  quantity: { value: 0 },
  class: 'bg-red-500/50 dark:bg-red-400/50 animate-pulse'
}]
</script>

<template>
  <UTable :rows="items" :columns="columns">
    <template #quantity-data="{ row }">
      {{ row.quantity.value }}
    </template>
  </UTable>
</template>

Sortable

Enable column sorting with the sortable property:

<script setup lang="ts">
const columns = [{
  key: 'name',
  label: 'Name',
  sortable: true
}, {
  key: 'email',
  label: 'Email',
  sortable: true,
  direction: 'desc'
}]
</script>

<template>
  <UTable :columns="columns" :rows="people" />
</template>

Default Sorting

Specify default sort settings with the sort prop:

<script setup lang="ts">
const sort = ref({
  column: 'name',
  direction: 'desc'
})
</script>

<template>
  <UTable :sort="sort" :columns="columns" :rows="people" />
</template>

Selectable

Make rows selectable using v-model:

<script setup lang="ts">
const selected = ref([people[1]])
</script>

<template>
  <UTable v-model="selected" :rows="people" />
</template>

Use by prop for object comparison and select event for row click handling:

<script setup lang="ts">
const selected = ref([])

function select(row) {
  const index = selected.value.findIndex(item => item.id === row.id)
  if (index === -1) {
    selected.value.push(row)
  } else {
    selected.value.splice(index, 1)
  }
}
</script>

<template>
  <UTable 
    v-model="selected" 
    :rows="people" 
    @select="select"
    by="id"
  />
</template>

Sortable

You can make the columns sortable by setting the sortable property to true in the column configuration.

You may specify the default direction of each column through the direction property. It can be either asc or desc, but it will default to asc.

<script setup lang="ts">
const columns = [{
  key: 'id',
  label: 'ID'
}, {
  key: 'name',
  label: 'Name',
  sortable: true
}, {
  key: 'title',
  label: 'Title',
  sortable: true
}, {
  key: 'email',
  label: 'Email',
  sortable: true,
  direction: 'desc' as const
}, {
  key: 'role',
  label: 'Role'
}]

const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}]
</script>

<template>
  <UTable :columns="columns" :rows="people" />
</template>

Default sorting

You can specify a default sort for the table through the sort prop. It’s an object with the following properties:

  • column - The column to sort by.
  • direction - The sort direction. Can be either asc or desc and defaults to asc.

This will set the default sort and will work even if no column is set as sortable.

<script setup lang="ts">
const sort = ref({
  column: 'name',
  direction: 'desc'
})

const columns = [{
  label: 'Name',
  key: 'name',
  sortable: true
}]

const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}]
</script>

<template>
  <UTable :sort="sort" :columns="columns" :rows="people" />
</template>

Reactive sorting

You can use a v-model:sort to make the sorting reactive. You may also use @update:sort to call your own function with the sorting data.

When fetching data from an API, we can take advantage of the useFetch or useAsyncData composables to fetch the data based on the sorting column and direction every time the sort reactive element changes.

When doing so, you might want to set the sort-mode prop to manual to disable the automatic sorting and return the rows as is.

<script setup lang="ts">
// Ensure it uses `ref` instead of `reactive`.
const sort = ref({
  column: 'name',
  direction: 'desc'
})

const columns = [{
  label: 'Name',
  key: 'name',
  sortable: true
}]

const { data, status } = await useLazyFetch(() => `/api/users?orderBy=${sort.value.column}&order=${sort.value.direction}`)
</script>

<template>
  <UTable v-model:sort="sort" :loading="status === 'pending'" :columns="columns" :rows="data" sort-mode="manual" />
</template>

We pass a function to useLazyFetch here to make the url reactive but you can use the query / params options alongside watch.

Custom sorting

Use the sort-button prop to customize the sort button in the header. You can pass all the props of the Button component to customize it through this prop or globally through ui.table.default.sortButton. Its icon defaults to i-heroicons-arrows-up-down-20-solid.

<template>
  <UTable
    sort-asc-icon="i-heroicons-arrow-up-20-solid"
    sort-desc-icon="i-heroicons-arrow-down-20-solid"
    :sort-button="{ icon: 'i-heroicons-sparkles-20-solid', color: 'primary', variant: 'outline', size: '2xs', square: false, ui: { rounded: 'rounded-full' } }"
    class="w-full"
    :columns="[{ key: 'id', label: 'ID' }, { key: 'name', label: 'Name', sortable: true }, { key: 'title', label: 'Title', sortable: true }, { key: 'email', label: 'Email', sortable: true }, { key: 'role', label: 'Role' }]"
    :rows="[{ id: 1, name: 'Lindsay Walton', title: 'Front-end Developer', email: 'lindsay.walton@example.com', role: 'Member' }, { id: 2, name: 'Courtney Henry', title: 'Designer', email: 'courtney.henry@example.com', role: 'Admin' }, { id: 3, name: 'Tom Cook', title: 'Director of Product', email: 'tom.cook@example.com', role: 'Member' }, { id: 4, name: 'Whitney Francis', title: 'Copywriter', email: 'whitney.francis@example.com', role: 'Admin' }, { id: 5, name: 'Leonard Krasner', title: 'Senior Designer', email: 'leonard.krasner@example.com', role: 'Owner' }, { id: 6, name: 'Floyd Miles', title: 'Principal Designer', email: 'floyd.miles@example.com', role: 'Member' }]"
  />
</template>

Use the sort-asc-icon prop to set a different icon or change it globally in ui.table.default.sortAscIcon. Defaults to i-heroicons-bars-arrow-up-20-solid.

Use the sort-desc-icon prop to set a different icon or change it globally in ui.table.default.sortDescIcon. Defaults to i-heroicons-bars-arrow-down-20-solid.

You can also customize the entire header cell, read more in the Slots section.

Selectable

Use a v-model to make the table selectable. The v-model will be an array of the selected rows.

<script setup lang="ts">
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}]

const selected = ref([people[1]])
</script>

<template>
  <UTable v-model="selected" :rows="people" />
</template>

You can use the by prop to compare objects by a field instead of comparing object instances. We’ve replicated the behavior of Headless UI Combobox.

You can also add a select listener on your Table to make the rows clickable. The function will receive the row as the first argument.

You can use this to navigate to a page, open a modal or even to select the row manually.

<script setup lang="ts">
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}]

function select(row) {
  const index = selected.value.findIndex(item => item.id === row.id)
  if (index === -1) {
    selected.value.push(row)
  } else {
    selected.value.splice(index, 1)
  }
}

const selected = ref([people[1]])
</script>

<template>
  <UTable v-model="selected" :rows="people" @select="select" />
</template>

Event Selectable

The UTable component provides two key events for handling row selection:

@select:all

The @select:all event is emitted when the header checkbox in a selectable table is toggled. This event returns a boolean value indicating whether all rows are selected (true) or deselected (false).

@update:modelValue

The @update:modelValue event is emitted whenever the selection state changes, including both individual row selection and bulk selection. This event returns an array containing the currently selected rows.

Here’s how to implement both events:

<script setup lang="ts">
const selected = ref([])

const onHandleSelectAll = (isSelected: boolean) => {
  console.log('All rows selected:', isSelected)
}

const onUpdateSelection = (selectedRows: any[]) => {
  console.log('Currently selected rows:', selectedRows)
}
</script>

<template>
  <UTable 
    v-model="selected" 
    :rows="people" 
    @select:all="onHandleSelectAll"
    @update:modelValue="onUpdateSelection"
  />
</template>

Single Select Mode

Control how the select function allows only one row to be selected at a time.

<template>
  <!-- Allow only one row to be selectable at a time -->
  <UTable :single-select="true" />
</template>

Checkbox Placement

You can customize the checkbox column position by using the select key in the columns configuration.

<script setup lang="ts">
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}]

const selected = ref([people[1]])

const columns = [{
  key: 'id',
  label: 'ID'
}, {
  key: 'name',
  label: 'User name'
}, {
  key: 'title',
  label: 'Job position'
}, {
  key: 'email',
  label: 'Email'
}, {
  key: 'role'
}, {
  key: 'select',
  class: 'w-2'
}]
</script>

<template>
  <UTable v-model="selected" :rows="people" :columns="columns" />
</template>

Contextmenu

Use the contextmenu listener on your Table to make the rows right-clickable. The function will receive the original event as the first argument and the row as the second argument.You can use this to open a ContextMenu for that row.

<script setup lang="ts">
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}]

const virtualElement = ref({ getBoundingClientRect: () => ({}) })
const contextMenuRow = ref()

function contextmenu(event: MouseEvent, row: any) {
  // Prevent the default context menu
  event.preventDefault()

  virtualElement.value.getBoundingClientRect = () => ({
    width: 0,
    height: 0,
    top: event.clientY,
    left: event.clientX
  })

  contextMenuRow.value = row
}
</script>

<template>
  <div>
    <UTable :rows="people" @contextmenu.stop="contextmenu" />

    <UContextMenu
      :virtual-element="virtualElement"
      :model-value="!!contextMenuRow"
      @update:model-value="contextMenuRow = null"
    >
      <div class="p-4">
        {{ contextMenuRow.id }} - {{ contextMenuRow.name }}
      </div>
    </UContextMenu>
  </div>
</template>

Searchable

You can easily use the Input component to filter the rows.

<script setup lang="ts">
const columns = [{
  key: 'id',
  label: 'ID'
}, {
  key: 'name',
  label: 'Name'
}, {
  key: 'title',
  label: 'Title'
}, {
  key: 'email',
  label: 'Email'
}, {
  key: 'role',
  label: 'Role'
}]

const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}]

const q = ref('')

const filteredRows = computed(() => {
  if (!q.value) {
    return people
  }

  return people.filter((person) => {
    return Object.values(person).some((value) => {
      return String(value).toLowerCase().includes(q.value.toLowerCase())
    })
  })
})
</script>

<template>
  <div>
    <div class="flex px-3 py-3.5 border-b border-gray-200 dark:border-gray-700">
      <UInput v-model="q" placeholder="Filter people..." />
    </div>

    <UTable :rows="filteredRows" :columns="columns" />
  </div>
</template>

Paginable

You can easily use the Pagination component to paginate the rows.

<script setup lang="ts">
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}, {
  id: 7,
  name: 'Emily Selman',
  title: 'VP, User Experience',
  email: '',
  role: 'Admin'
}, {
  id: 8,
  name: 'Kristin Watson',
  title: 'VP, Human Resources',
  email: '',
  role: 'Member'
}, {
  id: 9,
  name: 'Emma Watson',
  title: 'Front-end Developer',
  email: '',
  role: 'Member'
}, {
  id: 10,
  name: 'John Doe',
  title: 'Designer',
  email: '',
  role: 'Admin'
}, {
  id: 11,
  name: 'Jane Doe',
  title: 'Director of Product',
  email: '',
  role: 'Member'
}, {
  id: 12,
  name: 'John Smith',
  title: 'Copywriter',
  email: '',
  role: 'Admin'
}, {
  id: 13,
  name: 'Jane Smith',
  title: 'Senior Designer',
  email: '',
  role: 'Owner'
}]

const page = ref(1)
const pageCount = 5

const rows = computed(() => {
  return people.slice((page.value - 1) * pageCount, (page.value) * pageCount)
})
</script>

<template>
  <div>
    <UTable :rows="rows" />

    <div class="flex justify-end px-3 py-3.5 border-t border-gray-200 dark:border-gray-700">
      <UPagination v-model="page" :page-count="pageCount" :total="people.length" />
    </div>
  </div>
</template>

Expandable

You can use the v-model:expand to enables row expansion functionality in the table component. It maintains an object containing an openedRows an array and row an object, which tracks the indices of currently expanded rows.

When using the expand slot, you have access to the row property in the slot scope, which contains the data of the row that triggered the expand/collapse action. This allows you to customize the expanded content based on the row’s data.

<script setup lang='ts'>
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}]

const expand = ref({
  openedRows: [people[0]],
  row: {}
})
</script>

<template>
  <UTable v-model:expand="expand" :rows="people">
    <template #expand="{ row }">
      <div class="p-4">
        <pre>{{ row }}</pre>
      </div>
    </template>
  </UTable>
</template>

Event expand

The @update:expand event is emitted when a row is expanded. This event provides the current state of expanded rows and the data of the row that triggered the event.

To use the @update:expand event, add it to your UTable component. The event handler will receive an object with the following properties:

  • openedRows: An array of indices of the currently expanded rows.
  • row: The row data that triggered the expand/collapse action.
<script setup lang="ts">
const { data, pending } = await useLazyFetch(() => `/api/users`)

const handleExpand = ({ openedRows, row }) => {
  console.log('opened Rows:', openedRows);
  console.log('Row Data:', row);
};

const expand = ref({
  openedRows: [],
  row: null
})

</script>
<template>
  <UTable v-model="expand" :loading="pending" :rows="data" @update:expand="handleExpand">
    <template #expand="{ row }">
        <div class="p-4">
          <pre>{{ row }}</pre>
        </div>
      </template>
  </UTable>
</template>

Multiple expand

Controls whether multiple rows can be expanded simultaneously in the table.

<template>
  <!-- Allow only one row to be expanded at a time -->
  <UTable :multiple-expand="false" />

  <!-- Default behavior: Allow multiple rows to be expanded simultaneously -->
  <UTable :multiple-expand="true" />

  <!-- Or simply -->
  <UTable />
</template>

Disable Row Expansion

You can disable the expansion functionality for specific rows in the UTable component by adding the disabledExpand property to your row data.

Important: When using disabledExpand, you must define the columns prop for the UTable component. Otherwise, the table will render all properties as columns, including the disabledExpand property.

<script setup>
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin',
  disabledExpand: true
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin',
  disabledExpand: true
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member',
  disabledExpand: true
}]
const columns = [
  {
    label: 'Name',
    key: 'name'
  },
  {
    label: 'title',
    key: 'title'
  },
  {
    label: 'Email',
    key: 'email'
  },
  {
    label: 'role',
    key: 'role'
  }
]

const expand = ref({
  openedRows: [],
  row: null
})
</script>

<template>
  <UTable v-model:expand="expand" :rows="people" :columns="columns">
    <template #expand="{ row }">
      <div class="p-4">
        <pre>{{ row }}</pre>
      </div>
    </template>
  </UTable>
</template>

Loading

Use the loading prop to indicate that data is currently loading with an indeterminate Progress bar.

You can use the progress prop to customize the color and animation of the progress bar or change them globally in ui.table.default.progress (you can set it to null to hide the progress bar).

If there is no rows provided, a loading state will also be displayed. You can use the loading-state prop to customize the icon and label or change them globally in ui.table.default.loadingState (you can set it to null to hide the loading state).

<template>
  <UTable
    loading
    :loading-state="{ icon: 'i-heroicons-arrow-path-20-solid', label: 'Loading...' }"
    :progress="{ color: 'primary', animation: 'carousel' }"
    class="w-full"
    :columns="[{ key: 'id', label: 'ID' }, { key: 'name', label: 'Name' }, { key: 'title', label: 'Title' }, { key: 'email', label: 'Email' }, { key: 'role', label: 'Role' }]"
  />
</template>

This can be easily used with Nuxt useAsyncData composable.

<script setup lang="ts">
const columns = [...]

const { status, data: people } = await useLazyAsyncData('people', () => $fetch('/api/people'))
</script>

<template>
  <UTable :rows="people" :columns="columns" :loading="status === 'pending'" />
</template>

Empty

An empty state will be displayed when there are no results.

Use the empty-state prop to customize the icon and label or change them globally in ui.table.default.emptyState.

You can also set it to null to hide the empty state.

<template>
  <UTable
    :empty-state="{ icon: 'i-heroicons-circle-stack-20-solid', label: 'No items.' }"
    class="w-full"
    :columns="[{ key: 'id', label: 'ID' }, { key: 'name', label: 'Name' }, { key: 'title', label: 'Title' }, { key: 'email', label: 'Email' }, { key: 'role', label: 'Role' }]"
  />
</template>

Slots

You can use slots to customize the header and data cells of the table.

<column>-header

Use the #<column>-header slot to customize the header cell of a column. You will have access to the columnsort and on-sort properties in the slot scope.

The sort property is an object with the following properties:

  • field - The field to sort by.
  • direction - The direction to sort by. Can be asc or desc.

The on-sort property is a function that you can call to sort the table and accepts the column as parameter.

Even though you can customize the sort button as mentioned in the Sortable section, you can use this slot to completely override its behavior, with a custom dropdown for example.

<column>-data

Use the #<column>-data slot to customize the data cell of a column. You will have access to the��rowcolumn and getRowData properties in the slot scope.

You can for example create an extra column for actions with a Dropdown component inside or change the color of the rows based on a selection.

<script setup lang="ts">
const columns = [{
  key: 'name',
  label: 'Name'
}, {
  key: 'title',
  label: 'Title'
}, {
  key: 'email',
  label: 'Email'
}, {
  key: 'role',
  label: 'Role'
}, {
  key: 'actions'
}]

const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}]

const items = row => [
  [{
    label: 'Edit',
    icon: 'i-heroicons-pencil-square-20-solid',
    click: () => console.log('Edit', row.id)
  }, {
    label: 'Duplicate',
    icon: 'i-heroicons-document-duplicate-20-solid'
  }], [{
    label: 'Archive',
    icon: 'i-heroicons-archive-box-20-solid'
  }, {
    label: 'Move',
    icon: 'i-heroicons-arrow-right-circle-20-solid'
  }], [{
    label: 'Delete',
    icon: 'i-heroicons-trash-20-solid'
  }]
]

const selected = ref([people[1]])
</script>

<template>
  <UTable v-model="selected" :rows="people" :columns="columns">
    <template #name-data="{ row }">
      <span :class="[selected.find(person => person.id === row.id) && 'text-primary-500 dark:text-primary-400']">{{ row.name }}</span>
    </template>

    <template #actions-data="{ row }">
      <UDropdown :items="items(row)">
        <UButton color="gray" variant="ghost" icon="i-heroicons-ellipsis-horizontal-20-solid" />
      </UDropdown>
    </template>
  </UTable>
</template>

select-header

This slot allows you to customize the checkbox appearance in the table header for selecting all rows at once while using feature Selectable.

Usage

<template>
  <UTable v-model="selectable"> 
    <template #select-header="{ checked, change, indeterminate }">
      <!-- Place your custom component here -->
    </template>
  </UTable>
</template>

Props

Prop Type Description
checked Boolean Indicates if all rows are selected
change Function Function to handle selection state changes. Must receive a boolean value (true/false)
indeterminate Boolean Indicates partial selection (when some rows are selected)
<template>
  <UTable>
    <!-- Header checkbox customization -->
    <template #select-header="{ indeterminate, checked, change }">
      <input 
        type="checkbox"
        :indeterminate="indeterminate"
        :checked="checked"
        @change="e => change(e.target.checked)"
      />
    </template>
  </UTable>
</template>

select-data

This slot allows you to customize the checkbox appearance for each row in the table while using feature Selectable.

Usage

<template>
  <UTable v-model="selectable">
    <template #select-data="{ checked, change }">
      <!-- Place your custom component here -->
    </template>
  </UTable>
</template>

Props

Prop Type Description
checked Boolean Indicates if the current row is selected
change Function Function to handle selection state changes. Must receive a boolean value (true/false)

Example

<template>
  <UTable>
    <!-- Row checkbox customization -->
    <template #select-data="{ checked, change }">
      <input 
        type="checkbox"
        :checked="checked"
        @change="e => change(e.target.checked)"
      />
    </template>
  </UTable>
</template>

expand-action

The #expand-action slot allows you to customize the expansion control interface for expandable table rows. This feature provides a flexible way to implement custom expand/collapse functionality while maintaining access to essential row data and state.

Usage

<template>
  <UTable>
    <template #expand-action="{ row, toggle, isExpanded }">
    <!-- Your custom expand action content -->
    </template>
  </UTable>
</template>

Slot Props

The slot provides three key props:

Prop Type Description
row Object Contains the current row’s data
toggle Function Function to toggle the expanded state
isExpanded Boolean Current expansion state of the row
<script setup lang='ts'>
const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member',
  hasExpand: false
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin',
  hasExpand: true
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member',
  hasExpand: false
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin',
  hasExpand: true
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner',
  hasExpand: false
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member',
  hasExpand: true
}]

const expand = ref({
  openedRows: [people.find(v => v.hasExpand)],
  row: {}
})
</script>

<template>
  <UTable v-model:expand="expand" :rows="people">
    <template #expand="{ row }">
      <div class="p-4">
        <pre>{{ row }}</pre>
      </div>
    </template>
    <template #expand-action="{ row, isExpanded, toggle }">
      <UButton v-if="row.hasExpand" @click="toggle">
        {{ isExpanded ? 'collapse' : 'expand' }}
      </UButton>
    </template>
  </UTable>
</template>

loading-state

Use the #loading-state slot to customize the loading state.

<script setup lang="ts">
const columns = [{
  key: 'name',
  label: 'Name'
}, {
  key: 'title',
  label: 'Title'
}, {
  key: 'email',
  label: 'Email'
}, {
  key: 'role',
  label: 'Role'
}, {
  key: 'actions'
}]

const people = []

const pending = ref(true)
</script>

<template>
  <UTable :rows="people" :columns="columns" :loading="pending">
    <template #loading-state>
      <div class="flex items-center justify-center h-32">
        <i class="loader --6" />
      </div>
    </template>
  </UTable>
</template>

<style scoped>
/* https://codepen.io/jenning/pen/YzNmzaV */

.loader {
  --color: rgb(var(--color-primary-400));
  --size-mid: 6vmin;
  --size-dot: 1.5vmin;
  --size-bar: 0.4vmin;
  --size-square: 3vmin;

  display: block;
  position: relative;
  width: 50%;
  display: grid;
  place-items: center;
}

.loader::before,
.loader::after {
  content: '';
  box-sizing: border-box;
  position: absolute;
}

/**
loader --6
**/
.loader.--6::before {
  width: var(--size-square);
  height: var(--size-square);
  background-color: var(--color);
  top: calc(50% - var(--size-square));
  left: calc(50% - var(--size-square));
  animation: loader-6 2.4s cubic-bezier(0, 0, 0.24, 1.21) infinite;
}

@keyframes loader-6 {

  0%,
  100% {
    transform: none;
  }

  25% {
    transform: translateX(100%);
  }

  50% {
    transform: translateX(100%) translateY(100%);
  }

  75% {
    transform: translateY(100%);
  }
}
</style>

empty-state

Use the #empty-state slot to customize the empty state.

<script setup lang="ts">
const columns = [{
  key: 'name',
  label: 'Name'
}, {
  key: 'title',
  label: 'Title'
}, {
  key: 'email',
  label: 'Email'
}, {
  key: 'role',
  label: 'Role'
}, {
  key: 'actions'
}]

const people = []
</script>

<template>
  <UTable :rows="people" :columns="columns">
    <template #empty-state>
      <div class="flex flex-col items-center justify-center py-6 gap-3">
        <span class="italic text-sm">No one here!</span>
        <UButton label="Add people" />
      </div>
    </template>
  </UTable>
</template>

caption

Use the #caption slot to customize the table’s caption.

<script setup lang="ts">
const columns = [{
  key: 'name',
  label: 'Name'
}, {
  key: 'title',
  label: 'Title'
}, {
  key: 'email',
  label: 'Email'
}, {
  key: 'role',
  label: 'Role'
}, {
  key: 'actions'
}]

const people = [{
  id: 1,
  name: 'Lindsay Walton',
  title: 'Front-end Developer',
  email: 'lindsay.walton@example.com',
  role: 'Member'
}, {
  id: 2,
  name: 'Courtney Henry',
  title: 'Designer',
  email: 'courtney.henry@example.com',
  role: 'Admin'
}, {
  id: 3,
  name: 'Tom Cook',
  title: 'Director of Product',
  email: 'tom.cook@example.com',
  role: 'Member'
}, {
  id: 4,
  name: 'Whitney Francis',
  title: 'Copywriter',
  email: 'whitney.francis@example.com',
  role: 'Admin'
}, {
  id: 5,
  name: 'Leonard Krasner',
  title: 'Senior Designer',
  email: 'leonard.krasner@example.com',
  role: 'Owner'
}, {
  id: 6,
  name: 'Floyd Miles',
  title: 'Principal Designer',
  email: 'floyd.miles@example.com',
  role: 'Member'
}]
</script>

<template>
  <UTable :rows="people" :columns="columns">
    <template #caption>
      <caption>Employees of ACME</caption>
    </template>
  </UTable>
</template>

Props

Prop Type Default
sort `{ column: string; direction: “asc” “desc”; }`
ui object {}
caption string null
progress `{ color: string; animation: “carousel” “carousel-inverse”
modelValue unknown[] null
by `string Function`
rows TableRow[] []
loadingState { icon: string; label: string; } config.default.loadingState
emptyState { icon: string; label: string; } config.default.emptyState
expand Expanded<TableRow> null
columns TableColumn[] null
columnAttribute string "label"
sortMode `“auto” “manual”`
sortButton Button config.default.sortButton
sortAscIcon string config.default.sortAscIcon
sortDescIcon string config.default.sortDescIcon
expandButton Button config.default.expandButton
loading boolean false
multipleExpand boolean true
singleSelect boolean false

Config

{
  wrapper: 'relative overflow-x-auto',
  base: 'min-w-full table-fixed',
  divide: 'divide-y divide-gray-300 dark:divide-gray-700',
  thead: 'relative',
  tbody: 'divide-y divide-gray-200 dark:divide-gray-800',
  caption: 'sr-only',
  tr: {
    base: '',
    selected: 'bg-gray-50 dark:bg-gray-800/50',
    expanded: 'bg-gray-50 dark:bg-gray-800/50',
    active: 'hover:bg-gray-50 dark:hover:bg-gray-800/50 cursor-pointer'
  },
  th: {
    base: 'text-left rtl:text-right',
    padding: 'px-4 py-3.5',
    color: 'text-gray-900 dark:text-white',
    font: 'font-semibold',
    size: 'text-sm'
  },
  td: {
    base: 'whitespace-nowrap',
    padding: 'px-4 py-4',
    color: 'text-gray-500 dark:text-gray-400',
    font: '',
    size: 'text-sm'
  },
  checkbox: {
    padding: 'ps-4'
  },
  loadingState: {
    wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
    label: 'text-sm text-center text-gray-900 dark:text-white',
    icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4 animate-spin'
  },
  emptyState: {
    wrapper: 'flex flex-col items-center justify-center flex-1 px-6 py-14 sm:px-14',
    label: 'text-sm text-center text-gray-900 dark:text-white',
    icon: 'w-6 h-6 mx-auto text-gray-400 dark:text-gray-500 mb-4'
  },
  expand: {
    icon: 'transform transition-transform duration-200'
  },
  progress: {
    wrapper: 'absolute inset-x-0 -bottom-[0.5px] p-0'
  },
  default: {
    sortAscIcon: 'i-heroicons-bars-arrow-up-20-solid',
    sortDescIcon: 'i-heroicons-bars-arrow-down-20-solid',
    sortButton: {
      icon: 'i-heroicons-arrows-up-down-20-solid',
      trailing: true,
      square: true,
      color: 'gray',
      variant: 'ghost',
      class: '-m-1.5'
    },
    expandButton: {
      icon: 'i-heroicons-chevron-down',
      color: 'gray',
      variant: 'ghost',
      size: 'xs',
      class: '-my-1.5 align-middle'
    },
    checkbox: {
      color: 'primary'
    },
    progress: {
      color: 'primary',
      animation: 'carousel'
    },
    loadingState: {
      icon: 'i-heroicons-arrow-path-20-solid',
      label: 'Loading...'
    },
    emptyState: {
      icon: 'i-heroicons-circle-stack-20-solid',
      label: 'No items.'
    }
  }
}