Table

Tables display structured data in rows and columns with support for sorting, selection, column resizing, and infinite scrolling.

Usage

import { Table } from '@hareui/vue'
NameRoleStatusEmail
Kate MooreCEOActive[email protected]
John SmithCTOActive[email protected]
Sara JohnsonCMOOn Leave[email protected]
Michael BrownCFOActive[email protected]
<script setup lang="ts">import type { TableColumn, TableRow } from '@hareui/vue'import { Table } from '@hareui/vue'const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true },  { key: 'role', label: 'Role' },  { key: 'status', label: 'Status' },  { key: 'email', label: 'Email' },]const rows: TableRow[] = [  { id: 1, name: 'Kate Moore', role: 'CEO', status: 'Active', email: '[email protected]' },  { id: 2, name: 'John Smith', role: 'CTO', status: 'Active', email: '[email protected]' },  { id: 3, name: 'Sara Johnson', role: 'CMO', status: 'On Leave', email: '[email protected]' },  { id: 4, name: 'Michael Brown', role: 'CFO', status: 'Active', email: '[email protected]' },]</script><template>  <Table aria-label="Team members" :columns="columns" :rows="rows" :ui="{ content: 'min-w-[600px]' }" /></template>

Anatomy

HeroUI composes Table.ScrollContainer, Table.Content, Table.Header, Table.Column, Table.Body, Table.Row and Table.Cell. HareUI renders the same <table> structure from two arrays: columns (one entry per header) and rows (one object per row, looked up as row[column.key]). Per-column slots replace the default header or cell content.

<Table aria-label="Team members" :columns="columns" :rows="rows">
  <template #status-header="{ column, sortDirection }" /> <!-- optional: header content for the `status` column -->
  <template #status-cell="{ row, value }" /> <!-- optional: cell content for the `status` column -->
  <template #empty /> <!-- optional: shown when `rows` is empty -->
  <template #load-more /> <!-- optional: shown while `loading` (defaults to a Spinner) -->
  <template #footer /> <!-- optional: below the table, e.g. pagination -->
</Table>

ui keys: base, scrollContainer, resizableContainer, content, header, column, sortableColumnHeader, sortableColumnIndicator, body, row, cell, footer, columnResizer, loadMore, loadMoreContent.

Examples

Secondary Variant

NameRoleStatusEmail
Kate MooreCEOActive[email protected]
John SmithCTOActive[email protected]
Sara JohnsonCMOOn Leave[email protected]
Michael BrownCFOActive[email protected]
<script setup lang="ts">import type { TableColumn, TableRow } from '@hareui/vue'import { Table } from '@hareui/vue'const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true },  { key: 'role', label: 'Role' },  { key: 'status', label: 'Status' },  { key: 'email', label: 'Email' },]const rows: TableRow[] = [  { id: 1, name: 'Kate Moore', role: 'CEO', status: 'Active', email: '[email protected]' },  { id: 2, name: 'John Smith', role: 'CTO', status: 'Active', email: '[email protected]' },  { id: 3, name: 'Sara Johnson', role: 'CMO', status: 'On Leave', email: '[email protected]' },  { id: 4, name: 'Michael Brown', role: 'CFO', status: 'Active', email: '[email protected]' },]</script><template>  <Table aria-label="Team members" variant="secondary" :columns="columns" :rows="rows" :ui="{ content: 'min-w-[600px]' }" /></template>

Async Loading

Set has-more to render a sentinel row that emits loadMore when scrolled into view. While loading is true, the #load-more row is shown.

NameRoleStatusEmail
Kate MooreCEOActive[email protected]
John SmithCTOActive[email protected]
Sara JohnsonCMOOn Leave[email protected]
Michael BrownCFOActive[email protected]
Emily DavisProduct ManagerInactive[email protected]
Davis WilsonLead DesignerActive[email protected]
<script setup lang="ts">import type { TableColumn } from '@hareui/vue'import { Chip, Table } from '@hareui/vue'import { computed, ref } from 'vue'const statusColorMap = { 'Active': 'success', 'Inactive': 'danger', 'On Leave': 'warning' } as consttype Status = keyof typeof statusColorMapconst allUsers: { id: number, name: string, role: string, status: Status, email: string }[] = [  { id: 1, name: 'Kate Moore', role: 'CEO', status: 'Active', email: '[email protected]' },  { id: 2, name: 'John Smith', role: 'CTO', status: 'Active', email: '[email protected]' },  { id: 3, name: 'Sara Johnson', role: 'CMO', status: 'On Leave', email: '[email protected]' },  { id: 4, name: 'Michael Brown', role: 'CFO', status: 'Active', email: '[email protected]' },  { id: 5, name: 'Emily Davis', role: 'Product Manager', status: 'Inactive', email: '[email protected]' },  { id: 6, name: 'Davis Wilson', role: 'Lead Designer', status: 'Active', email: '[email protected]' },  { id: 7, name: 'Olivia Martinez', role: 'Frontend Engineer', status: 'Active', email: '[email protected]' },  { id: 8, name: 'James Taylor', role: 'Backend Engineer', status: 'Active', email: '[email protected]' },  { id: 9, name: 'Sophia Anderson', role: 'QA Engineer', status: 'On Leave', email: '[email protected]' },  { id: 10, name: 'Liam Thomas', role: 'DevOps Engineer', status: 'Active', email: '[email protected]' },  { id: 11, name: 'Lucas Martinez', role: 'Product Manager', status: 'Active', email: '[email protected]' },  { id: 12, name: 'Emma Johnson', role: 'Frontend Engineer', status: 'Active', email: '[email protected]' },  { id: 13, name: 'Noah Davis', role: 'Backend Engineer', status: 'Active', email: '[email protected]' },  { id: 14, name: 'Ava Wilson', role: 'Lead Designer', status: 'Active', email: '[email protected]' },  { id: 15, name: 'Oliver Martinez', role: 'Frontend Engineer', status: 'Active', email: '[email protected]' },  { id: 16, name: 'Isabella Johnson', role: 'Backend Engineer', status: 'Active', email: '[email protected]' },  { id: 17, name: 'Mia Davis', role: 'Lead Designer', status: 'Active', email: '[email protected]' },  { id: 18, name: 'William Wilson', role: 'Frontend Engineer', status: 'Active', email: '[email protected]' },]const ITEMS_PER_PAGE = 6const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true },  { key: 'role', label: 'Role' },  { key: 'status', label: 'Status' },  { key: 'email', label: 'Email' },]const items = ref(allUsers.slice(0, ITEMS_PER_PAGE))const loading = ref(false)const hasMore = computed(() => items.value.length < allUsers.length)function loadMore() {  if (!hasMore.value || loading.value)    return  loading.value = true  setTimeout(() => {    items.value = allUsers.slice(0, items.value.length + ITEMS_PER_PAGE)    loading.value = false  }, 1500)}</script><template>  <Table    aria-label="Async loading table"    :columns="columns"    :has-more="hasMore"    :loading="loading"    :rows="items"    :ui="{      scrollContainer: 'h-[280px] overflow-y-auto',      content: 'min-w-[600px]',      header: 'sticky top-0 z-10 bg-surface-secondary',    }"    @load-more="loadMore"  >    <template #status-cell="{ row }">      <Chip :color="statusColorMap[row.status as Status]" size="sm" variant="soft">{{ row.status }}</Chip>    </template>  </Table></template>

Sorting

Set sortable on a column to render the sort indicator and make the header toggle the sort. Bind v-model:sort-descriptor and pass rows already sorted: the table displays the sort state, it does not sort the data.

NameRoleStatusEmail
Emily DavisProduct ManagerInactive[email protected]
John SmithCTOActive[email protected]
Kate MooreCEOActive[email protected]
Michael BrownCFOActive[email protected]
Sara JohnsonCMOOn Leave[email protected]
<script setup lang="ts">import type { TableColumn, TableSortDescriptor } from '@hareui/vue'import { Table } from '@hareui/vue'import { computed, ref } from 'vue'const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true, sortable: true },  { key: 'role', label: 'Role', sortable: true },  { key: 'status', label: 'Status', sortable: true },  { key: 'email', label: 'Email', sortable: true },]const users = [  { id: 1, name: 'Kate Moore', role: 'CEO', status: 'Active', email: '[email protected]' },  { id: 2, name: 'John Smith', role: 'CTO', status: 'Active', email: '[email protected]' },  { id: 3, name: 'Sara Johnson', role: 'CMO', status: 'On Leave', email: '[email protected]' },  { id: 4, name: 'Michael Brown', role: 'CFO', status: 'Active', email: '[email protected]' },  { id: 5, name: 'Emily Davis', role: 'Product Manager', status: 'Inactive', email: '[email protected]' },]const sortDescriptor = ref<TableSortDescriptor>({ column: 'name', direction: 'ascending' })const rows = computed(() => {  const { column, direction } = sortDescriptor.value  const sorted = [...users].sort((a, b) => String(a[column as keyof typeof a]).localeCompare(String(b[column as keyof typeof b])))  return direction === 'descending' ? sorted.reverse() : sorted})</script><template>  <Table    v-model:sort-descriptor="sortDescriptor"    aria-label="Table with sorting"    :columns="columns"    :rows="rows"    :ui="{ content: 'min-w-[600px]' }"  /></template>

Selection

Enable row selection with selection-mode. The table adds a checkbox column with a select-all checkbox in the header; bind v-model:selected-keys to read or control the selection ('all' or a Set of row ids).

NameRoleStatusEmail
Kate MooreCEOActive[email protected]
John SmithCTOActive[email protected]
Sara JohnsonCMOOn Leave[email protected]
Michael BrownCFOActive[email protected]

Selected: None

<script setup lang="ts">import type { TableColumn, TableRow, TableSelection } from '@hareui/vue'import { Table } from '@hareui/vue'import { ref } from 'vue'const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true },  { key: 'role', label: 'Role' },  { key: 'status', label: 'Status' },  { key: 'email', label: 'Email' },]const rows: TableRow[] = [  { id: 1, name: 'Kate Moore', role: 'CEO', status: 'Active', email: '[email protected]' },  { id: 2, name: 'John Smith', role: 'CTO', status: 'Active', email: '[email protected]' },  { id: 3, name: 'Sara Johnson', role: 'CMO', status: 'On Leave', email: '[email protected]' },  { id: 4, name: 'Michael Brown', role: 'CFO', status: 'Active', email: '[email protected]' },]const selectedKeys = ref<TableSelection>(new Set())</script><template>  <div class="flex flex-col gap-3">    <Table      v-model:selected-keys="selectedKeys"      aria-label="Table with selection"      :columns="columns"      :rows="rows"      selection-mode="multiple"      :ui="{ content: 'min-w-[600px]' }"    />    <p class="text-sm text-muted">      Selected:      <span class="font-medium">{{ selectedKeys === 'all' ? 'All' : selectedKeys.size > 0 ? [...selectedKeys].join(', ') : 'None' }}</span>    </p>  </div></template>

Expandable Rows

Rows with children are nested. Use tree-column to designate the column that is indented by depth, and render a toggle in that column's cell slot with the toggle, isExpanded and hasChildItems slot props. Use v-model:expanded-keys to control which rows are expanded.

NameTypeDate Modified
DocumentsDirectory10/20/2025
ProjectDirectory8/2/2025
PhotosDirectory2/3/2026
<script setup lang="ts">import type { TableColumn, TableRow, TableSelection } from '@hareui/vue'import { Button, Table } from '@hareui/vue'import { Icon } from '@iconify/vue'import { ref } from 'vue'const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true },  { key: 'type', label: 'Type' },  { key: 'date', label: 'Date Modified' },]const rows: TableRow[] = [  {    id: '1',    name: 'Documents',    type: 'Directory',    date: '10/20/2025',    children: [      {        id: '2',        name: 'Project',        type: 'Directory',        date: '8/2/2025',        children: [          { id: '3', name: 'Weekly Report', type: 'File', date: '7/10/2025' },          { id: '4', name: 'Budget', type: 'File', date: '8/20/2025' },        ],      },    ],  },  {    id: '5',    name: 'Photos',    type: 'Directory',    date: '2/3/2026',    children: [      { id: '6', name: 'Image 1', type: 'File', date: '1/23/2026' },      { id: '7', name: 'Image 2', type: 'File', date: '2/3/2026' },    ],  },]const expandedKeys = ref<TableSelection>(new Set(['1']))</script><template>  <Table    v-model:expanded-keys="expandedKeys"    aria-label="Files"    :columns="columns"    :rows="rows"    tree-column="name"    :ui="{ content: 'min-w-[520px]' }"  >    <template #name-cell="{ row, hasChildItems, isExpanded, toggle }">      <span class="flex items-center gap-1">        <Button v-if="hasChildItems" aria-label="Toggle row" icon-only size="sm" variant="ghost" @click="toggle">          <Icon            aria-hidden="true"            :class="['size-4 text-muted transition-transform duration-150', isExpanded ? 'rotate-90' : 'rtl:rotate-180']"            icon="gravity-ui:chevron-right"          />        </Button>        <span>{{ row.name }}</span>      </span>    </template>  </Table></template>

Column Resizing

Set resizable on a column to add a column resizer to its header. As with HeroUI's ResizableContainer, the columns of a resizable table share the container's width: width sets each column's initial size (pixels, '25%' or '2fr', default '1fr'), clamped by minWidth (default 75) and maxWidth. Resizing a column fixes the columns to its left at their current width.

NameRoleStatusEmail
Kate MooreCEOActive[email protected]
John SmithCTOActive[email protected]
Sara JohnsonCMOOn Leave[email protected]
Michael BrownCFOActive[email protected]
Emily DavisProduct ManagerInactive[email protected]
<script setup lang="ts">import type { TableColumn, TableRow } from '@hareui/vue'import { Chip, Table } from '@hareui/vue'const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true, minWidth: 160, resizable: true },  { key: 'role', label: 'Role', minWidth: 220, resizable: true },  { key: 'status', label: 'Status', minWidth: 100, resizable: true },  { key: 'email', label: 'Email', minWidth: 200 },]const rows: TableRow[] = [  { id: 1, name: 'Kate Moore', role: 'CEO', status: 'Active', color: 'success', email: '[email protected]' },  { id: 2, name: 'John Smith', role: 'CTO', status: 'Active', color: 'success', email: '[email protected]' },  { id: 3, name: 'Sara Johnson', role: 'CMO', status: 'On Leave', color: 'warning', email: '[email protected]' },  { id: 4, name: 'Michael Brown', role: 'CFO', status: 'Active', color: 'success', email: '[email protected]' },  { id: 5, name: 'Emily Davis', role: 'Product Manager', status: 'Inactive', color: 'danger', email: '[email protected]' },]</script><template>  <Table aria-label="Table with resizable columns" :columns="columns" :rows="rows" :ui="{ content: 'min-w-[700px]' }">    <template #status-cell="{ row }">      <Chip :color="row.color" size="sm" variant="soft">{{ row.status }}</Chip>    </template>  </Table></template>

Empty State

Use the #empty slot to display a custom message when the table has no data.

NameRoleStatusEmail
No results found
<script setup lang="ts">import type { TableColumn } from '@hareui/vue'import { EmptyState, Table } from '@hareui/vue'import { Icon } from '@iconify/vue'const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true },  { key: 'role', label: 'Role' },  { key: 'status', label: 'Status' },  { key: 'email', label: 'Email' },]</script><template>  <Table aria-label="Empty table" class="min-h-[200px]" :columns="columns" :rows="[]" :ui="{ content: 'h-full min-w-[600px]' }">    <template #empty>      <EmptyState class="flex h-full w-full flex-col items-center justify-center gap-4 text-center">        <Icon class="size-6 text-muted" icon="gravity-ui:tray" />        <span class="text-sm text-muted">No results found</span>      </EmptyState>    </template>  </Table></template>

Custom Cells

Worker IDMemberRoleStatusActions
#1234567
ED
Product ManagerInactive
#5273849
JS
Chief Technology OfficerActive
#4586932
KM
Chief Executive OfficerActive
#8293746
MB
Michael Brown[email protected]
Chief Financial OfficerActive
#7492836
SJ
Sara Johnson[email protected]
Chief Marketing OfficerOn Leave
<script setup lang="ts">import type { TableColumn, TableSelection, TableSortDescriptor } from '@hareui/vue'import { Avatar, Button, Chip, Table } from '@hareui/vue'import { Icon } from '@iconify/vue'import { computed, ref } from 'vue'const statusColorMap = { 'Active': 'success', 'Inactive': 'danger', 'On Leave': 'warning' } as consttype Status = keyof typeof statusColorMapconst AVATARS = 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars'const users: { id: number, name: string, image_url: string, role: string, status: Status, email: string }[] = [  { id: 4586932, name: 'Kate Moore', image_url: `${AVATARS}/red.jpg`, role: 'Chief Executive Officer', status: 'Active', email: '[email protected]' },  { id: 5273849, name: 'John Smith', image_url: `${AVATARS}/green.jpg`, role: 'Chief Technology Officer', status: 'Active', email: '[email protected]' },  { id: 7492836, name: 'Sara Johnson', image_url: `${AVATARS}/blue.jpg`, role: 'Chief Marketing Officer', status: 'On Leave', email: '[email protected]' },  { id: 8293746, name: 'Michael Brown', image_url: `${AVATARS}/purple.jpg`, role: 'Chief Financial Officer', status: 'Active', email: '[email protected]' },  { id: 1234567, name: 'Emily Davis', image_url: `${AVATARS}/orange.jpg`, role: 'Product Manager', status: 'Inactive', email: '[email protected]' },]const columns: TableColumn[] = [  { key: 'id', label: 'Worker ID', isRowHeader: true, sortable: true, class: 'after:hidden' },  { key: 'name', label: 'Member', sortable: true },  { key: 'role', label: 'Role', sortable: true, class: 'min-w-52' },  { key: 'status', label: 'Status', sortable: true, class: 'min-w-25' },  { key: 'actions', label: 'Actions' },]const selectedKeys = ref<TableSelection>(new Set())const sortDescriptor = ref<TableSortDescriptor>({ column: 'name', direction: 'ascending' })const rows = computed(() => {  const { column, direction } = sortDescriptor.value  return [...users].sort((a, b) => {    const cmp = String(a[column as keyof typeof a]).localeCompare(String(b[column as keyof typeof b]))    return direction === 'descending' ? -cmp : cmp  })})const initials = (name: string) => name.split(' ').map(n => n[0]).join('')</script><template>  <Table    v-model:selected-keys="selectedKeys"    v-model:sort-descriptor="sortDescriptor"    aria-label="Table with custom cells"    :columns="columns"    :rows="rows"    selection-mode="multiple"    :ui="{ content: 'min-w-[800px]' }"  >    <template #actions-header>      <span class="block text-end">Actions</span>    </template>    <template #id-cell="{ row }">      <div class="flex items-center gap-2 font-medium">        #{{ row.id }}        <Button :aria-label="`Copy ID #${row.id}`" icon-only size="sm" variant="ghost">          <Icon class="size-4 text-muted" icon="gravity-ui:copy" />        </Button>      </div>    </template>    <template #name-cell="{ row }">      <div class="flex items-center gap-3">        <Avatar :fallback="initials(row.name)" size="sm" :src="row.image_url" />        <div class="flex flex-col">          <span class="text-xs">{{ row.name }}</span>          <span class="text-xs text-muted">{{ row.email }}</span>        </div>      </div>    </template>    <template #status-cell="{ row }">      <Chip :color="statusColorMap[row.status as Status]" size="sm" variant="soft">{{ row.status }}</Chip>    </template>    <template #actions-cell="{ row }">      <div class="flex items-center gap-1">        <Button :aria-label="`View ${row.name}`" icon-only size="sm" variant="tertiary">          <Icon class="size-4" icon="gravity-ui:eye" />        </Button>        <Button :aria-label="`Edit ${row.name}`" icon-only size="sm" variant="tertiary">          <Icon class="size-4" icon="gravity-ui:pencil" />        </Button>        <Button :aria-label="`Delete ${row.name}`" icon-only size="sm" variant="danger-soft">          <Icon class="size-4" icon="gravity-ui:trash-bin" />        </Button>      </div>    </template>  </Table></template>

Customization

Tailwind CSS

Use the class prop for the root, ui for every part, and column.class for one column's header and cells:

NameRoleStatus
Kate MooreCEOActive
John SmithCTOActive
Sara JohnsonCMOOn leave
<script setup lang="ts">import type { TableColumn, TableRow } from '@hareui/vue'import { Table } from '@hareui/vue'const columns: TableColumn[] = [  { key: 'name', label: 'Name', isRowHeader: true },  { key: 'role', label: 'Role' },  { key: 'status', label: 'Status' },]const rows: TableRow[] = [  { id: 1, name: 'Kate Moore', role: 'CEO', status: 'Active' },  { id: 2, name: 'John Smith', role: 'CTO', status: 'Active' },  { id: 3, name: 'Sara Johnson', role: 'CMO', status: 'On leave' },]</script><template>  <Table    aria-label="Team members"    class="w-full max-w-2xl overflow-hidden rounded-xl border border-border/80 shadow-sm ring-1 ring-black/5 dark:ring-white/10"    :columns="columns"    :rows="rows"    :ui="{      content: 'min-w-[600px]',      header: 'bg-neutral-100/80 dark:bg-neutral-800/80',      column: 'text-xs font-semibold uppercase tracking-wide text-neutral-500 dark:text-neutral-400',      row: 'border-t border-border/60 transition-colors hover:bg-neutral-100/60 even:bg-neutral-50/50 dark:hover:bg-neutral-800/50 dark:even:bg-neutral-900/30',    }"  >    <template #name-cell="{ value }">      <span class="font-medium text-neutral-900 dark:text-neutral-100">{{ value }}</span>    </template>  </Table></template>

Global theme override

Override the table theme app-wide with createHareUI:

app.use(createHareUI({
  ui: {
    table: {
      slots: {
        header: 'bg-gray-100',
        column: 'px-4 py-2.5 text-left text-xs font-medium text-gray-600',
        row: 'border-b border-gray-200',
        cell: 'bg-white px-4 py-3 text-sm',
      },
    },
  },
}))

Styling Reference

Each rendered element carries the same data-slot string as HeroUI, so selectors translate 1:1.

ui keydata-slotDescription
basetableRoot container
scrollContainertable-scroll-containerHorizontal scroll wrapper
resizableContainertable-resizable-containerScroll wrapper used when a column is resizable
contenttable-contentThe <table> element
headertable-headerThe <thead> element
columntable-columnHeader cell
sortableColumnHeadertable-sortable-column-headerLabel and indicator of a sortable column
sortableColumnIndicatortable-sortable-column-indicatorSort direction chevron
bodytable-bodyThe <tbody> element
rowtable-rowBody row
celltable-cellBody cell
columnResizertable-column-resizerColumn resize handle
loadMoretable-load-moreRow shown while loading more
loadMoreContenttable-load-more-contentContent wrapper of the load-more row
footertable-footerFooter below the table

HeroUI's modifier classes correspond to props (HareUI renders utilities, not BEM classes, so style through class, ui or createHareUI):

  • variant: primary (default, rows on a secondary surface), secondary (.table-root--{variant})

Interactive States

  • Hover: :hover (row background change)
  • Selected: [data-selected="true"] (row highlight)
  • Focus: :focus-visible (inset focus ring on rows and sortable columns)
  • Disabled: [aria-disabled="true"] (reduced opacity)
  • Sortable: [data-allows-sorting="true"] (interactive cursor on columns)

API Reference

Props

PropTypeDefaultDescription
columnsTableColumn[]-Column definitions, in display order.
rowsTableRow[]-Row data, already sorted by the caller according to sortDescriptor (Table only displays the sort indicator and emits changes).
ariaLabelstring-Accessible label for the table.
variant"primary" | "secondary""primary"Visual variant. Primary has a gray background container with a card-style body; secondary is flat with transparent rows.
selectionMode"multiple" | "single" | "none""none"Row selection behavior. Adds a checkbox column when not 'none'.
defaultSelectedKeysTableSelectionnew Set()Selected row keys when uncontrolled.
disabledKeysTableSelectionKey[]-Row keys excluded from selection, in addition to any row with disabled.
defaultSortDescriptorTableSortDescriptor-Sort state when uncontrolled.
treeColumnstring-Key of the column that renders indentation for nested rows and receives tree-related slot props.
defaultExpandedKeysTableSelectionnew Set()Expanded row keys when uncontrolled.
hasMorebooleanfalseAdds the load-more sentinel at the end of the body; loadMore is emitted when it scrolls into view.
loadingbooleanfalseShows the #load-more row (a Spinner by default) and stops re-emitting loadMore while the sentinel stays visible.
uiComponentSlots<{ slots: { base: string; scrollContainer: string; resizableContainer: string; content: string; header: string; column: string[]; sortableColumnHeader: string; sortableColumnIndicator: string; body: string; row: string[]; cell: string[]; footer: string; columnResizer: string[]; loadMore: string; loadMoreContent: string; }; variants: { variant: { primary: { base: string; content: string; body: string[]; }; secondary: { header: string; column: string; body: string; row: string; cell: string; }; }; }; defaultVariants: { variant: string; }; }>-Per-slot class overrides.
selectedKeysTableSelection-Controlled selected row keys. Use v-model:selected-keys.
expandedKeysTableSelection-Controlled expanded row keys (tree rows). Use v-model:expanded-keys.
sortDescriptorTableSortDescriptor-Controlled sort state. Use v-model:sort-descriptor.

Slots

SlotPropsDescription
emptyanyContent shown instead of the body when rows is empty.
load-moreanyContent of the load-more row shown while loading. Defaults to a Spinner.
footeranyFooter content, rendered outside the scrollable table (e.g. pagination).

Emits

EventPayloadDescription
loadMore[]-
update:selectedKeys[value: TableSelection | undefined]-
update:expandedKeys[value: TableSelection | undefined]-
update:sortDescriptor[value: TableSortDescriptor | undefined]-

TableColumn

PropTypeDescription
keystringUnique column key, looked up as row[key]
labelstringHeader text
sortablebooleanShows the sort indicator; clicking the header updates sortDescriptor
isRowHeaderbooleanRenders this column's cells as row headers (role="rowheader")
align'start' | 'center' | 'end'Text alignment of the header and cells
widthnumber | '${number}%' | '${number}fr'Initial width; percentages and fr apply to resizable tables only
minWidth / maxWidthnumberWidth limits in px for resizable tables (minWidth defaults to 75)
resizablebooleanAdds a column resizer
classanyClasses for this column's header and cells

TableRow

PropTypeDescription
idstring | numberRow key used by selection and expansion (defaults to the row index)
childrenTableRow[]Nested rows (with tree-column)
disabledbooleanExcludes the row from selection

On this page

No Headings