Table

A powerful, responsive table and datagrids built using Tanstack

Examples

Basic

PropDefaultTypeDescription
columns[]ArrayTable columns.
data[]ArrayTable data.
Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
AlishaFisher25873relationship80
JohnpaulDibbert2339single27
EmilianoConnelly-Flatley23265complicated42
AnitaKing3930relationship50
MetaWiegand36997single10

Row Selection

Row selection allows you to select rows in the table. This is useful when you want to select rows in the table.

PropDefaultTypeDescription
modelValue[]ArraySelected rows.
enableRowSelectionfalseBooleanEnable row selection.
enableMultiRowSelectionfalseBooleanEnable multiple row selection.
rowIdidStringRow id to uniquely identify each row.
enableSubRowSelectionfalseBooleanEnable sub row selection.
@select-EventEmitted when a row is selected.
@select-all-EventEmitted when all rows are selected.
Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
EmmetWeimann17918single43
ZanderMedhurst3861single47
KyleighTillman679complicated90
FlorineStreich38440relationship90
MaidaDuBuque25920complicated0
JewelFahey40818relationship85
JaylinGoyette28774single49
AidanStanton34292complicated3
ArnoldoBlick12873relationship46
FlavieHeaney34846complicated55
of row(s) selected.

Loading

Loading allows you to show a loading progress indicator in the table. This is useful when you want to show a loading progress indicator in the table.

PropDefaultTypeDescription
loadingfalseBooleanLoading state.
Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
EnosKub4909complicated99
RaheemConsidine39795complicated92
VaughnSchaefer30839single65
TressaJohnson15170complicated68
CarlottaGoyette29797complicated84
MontyCarter13831single4
KristyParisian35624complicated80
ImogeneGleichner30429single25
RyannWeissnat26499single11
AnselRaynor9994complicated91

Pagination

Pagination allows you to paginate rows in the table. This is useful when you want to paginate rows in the table.

PropDefaultTypeDescription
pagination{pageIndex: 0, pageSize: 10}{pageIndex: Number, pageSize: Number}Pagination default configuration.
manualPaginationfalseBooleanEnable manual pagination.
Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
CarliOrtiz1697single66
AdrianBednar0539complicated75
EveMuller4672complicated9
StoneRogahn9167single63
KeshawnRyan20476relationship100
Page 1 of

Sorting

Sorting allows you to sort columns in ascending or descending order. This is useful when you want to sort columns in the table.

PropDefaultTypeDescription
enableSorting-booleanEnable all column sorting
column.enableSorting-booleanEnable specific column sorting
enableMultiColumnSort-booleanEnable multi-column sorting
Preview
Code
Data
Status
FriedrichSchmeler16836complicated30
BillieHyatt21343complicated54
TressaMcLaughlin23627relationship13
RosaliaGerlach33874single27
MyraBarton-Kling12371single25
TerrenceVon25635single1
PierceWelch26646relationship41
JamarcusPredovic12208complicated64
ShaunLeffler15438single46
RebeccaFeest22single20

Visibility

Visibility allows you to show or hide columns in the table. This is useful when you want to show or hide columns in the table.

PropDefaultTypeDescription
columnVisibility-objectThe column visibility state
Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
IzaiahBotsford7463relationship19
QuintonLebsack19716single92
JaidaHyatt29110complicated33
AnnaReynolds33330single35
DavonLindgren1973relationship55

Global Filtering

Global filtering allows you to filter rows based on the value entered in the filter input. This is useful when you want to filter rows in the table.

PropDefaultTypeDescription
globalFilter-stringThe global filter value
Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
TressaRutherford18745relationship94
WaltonBechtelar33454complicated37
SisterYost24480complicated19
ChelseyWest22857single12
ReynoldPadberg22278complicated73
WardCartwright6249complicated7
JazminMills26417single50
WalterWilliamson2396complicated47
CamillaStrosin374relationship48
JeanCarroll22638relationship65

Column Filtering

Column filtering allows you to filter columns based on the value entered in the filter input. This is useful when you want to filter columns in the table.

PropDefaultTypeDescription
enableColumnFilter-booleanEnable all column filtering
column.enableColumnFilter-booleanEnable specific column filtering
Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
RyannTurner25923single37
MagaliSwift-Lind3310single61
BennettKeeling23975relationship50
GreysonCorwin4407complicated78
WilberAbernathy18917complicated52

Column Ordering

Column ordering allows you to reorder columns by dragging and dropping them. This is useful when you want to change the order of columns in the table.

Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
WalkerMetz15702relationship34
WainoGreenholt11106relationship90
AmbroseBorer39103complicated81
BlazeHaag30798relationship86
VidaBode18211single83

Column Pinning

Column pinning allows you to pin columns to the left or right of the table. This is useful when you have a large number of columns and you want to keep some columns in view while scrolling.

PropDefaultTypeDescription
columnPins-{ left: Array, right: Array }Defines which columns are pinned to the left or right.
Preview
Code
Data
StatusFirst NameLast NameAgeVisitsProfile Progress
relationshipMarielleBruen98848
relationshipMikelWillms884758
complicatedSylvanPollich126317
relationshipMosesBerge2868
relationshipDedricBruen1669752

Expanding

Expanding allows you to expand rows to show additional information. This is useful when you want to show additional information about a row.

Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
LennyBednar24716single70
RetaWuckert21830relationship90
AubreeAnkunding-Klocko0817relationship30
RaleighLittel2603relationship56
ClaudieSchmitt34194complicated63

Grouping

Grouping allows you to group rows based on a column value. This is useful when you want to group rows in the table.

Preview
Code
Data
NameInfo
First NameLast NameAgeVisitsStatusProfile Progress
MellieGottlieb27341single68
BrodySatterfield11645single87
MohamedSimonis10696complicated5
FrancoLubowitz10135complicated17
RossieOrtiz-Flatley1667complicated17

Server-side

Allows you to fetch data from the server. This is useful when you want to fetch data from the server.

Preview
Code
Data
NameUrl
bulbasaurhttps://pokeapi.co/api/v2/pokemon/1/
ivysaurhttps://pokeapi.co/api/v2/pokemon/2/
venusaurhttps://pokeapi.co/api/v2/pokemon/3/
charmanderhttps://pokeapi.co/api/v2/pokemon/4/
charmeleonhttps://pokeapi.co/api/v2/pokemon/5/
Page 1 of 261

Customization

Configure the progress using the una prop and utility classes.

PropDefaultTypeDescription
columns.meta.una{}ObjectColumn Una meta data.
una{}ObjectGlobal Una attribute.
Preview
Code
Data
First NameLast NameAgeVisitsStatusProfile Progress
MayeTorphy25608single39
PierreBotsford0448complicated26
VioletteReilly-Rolfson24995relationship80
PinkieSteuber1894single4
GarretJacobs2188single21

Slots

NamePropsDescription
{column}-filtercolumnColumn filter slot.
{column}-headercolumnColumn header slot.
{column}-cellcellColumn cell slot.
{column}-footercolumnColumn footer slot.
headertableHeader slot.
bodytableBody slot.
rawrowRow slot.
footertableFooter slot.
expandedrowExpanded slot.
empty-Empty slot.
loading-Loading slot.
Preview
Code
Data
Account
EH
Eldred Huel
Richmond_Green3@hotmail.com
EldredHuelrelationship
38%
OO
Orin Olson
Korey.Windler49@hotmail.com
OrinOlsonrelationship
95%
LA
Lexus Ankunding
Arnaldo_Heaney70@yahoo.com
LexusAnkundingcomplicated
90%
KS
Keenan Schuppe
Keara_Kohler@gmail.com
KeenanSchuppecomplicated
56%
RH
Rachael Herman
Mina.Muller88@gmail.com
RachaelHermancomplicated
59%
BH
Beth Hoeger
Bella28@hotmail.com
BethHoegerrelationship
78%
RW
Rachael Walter
Cristal74@yahoo.com
RachaelWaltersingle
3%
CW
Christophe Walker
Amaya.Padberg@yahoo.com
ChristopheWalkercomplicated
28%
IK
Imani Kihn
Keeley_Dickens1@hotmail.com
ImaniKihncomplicated
13%
BO
Beth Osinski
Nils_Sipes51@gmail.com
BethOsinskirelationship
8%
Page 1 of

Presets

shortcuts/table.ts
type TablePrefix = 'table'

export const staticTable: Record<`${TablePrefix}-${string}` | TablePrefix, string> = {
  // config
  'table-default-variant': 'table-solid-gray',
  'table': '',

  // table-root
  'table-root-wrapper': 'relative w-full overflow-x-auto overflow-y-hidden border border-base rounded-md',
  'table-root': 'w-full caption-bottom text-sm',
  'table-body': '[&_tr:last-child]:border-0',
  'table-caption': 'mt-4 text-sm text-muted',

  // table-head
  'table-head': 'h-12 px-4 text-left align-middle font-medium text-muted [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-0.5',
  'table-head-pinned': 'sticky bg-base',
  'table-head-pinned-left': 'left-0',
  'table-head-pinned-right': 'right-0',

  // table-header
  'table-header': '[&_tr]:border-b [&_tr]:border-base',

  // table-row
  'table-row': 'border-b border-base transition-colors hover:bg-muted data-[filter=true]:hover:bg-base data-[state=selected]:bg-muted',

  // table-cell
  'table-cell': 'p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-0.5',
  'table-cell-pinned': 'sticky bg-base',
  'table-cell-pinned-left': 'left-0',
  'table-cell-pinned-right': 'right-0',

  // table-empty
  'table-empty-row': '',
  'table-empty-cell': 'p-4 whitespace-nowrap align-middle text-sm text-muted bg-base',
  'table-empty': 'flex items-center justify-center py-10',

  // table-loading
  'table-loading-icon': 'animate-spin text-lg', // TODO: to add
  'table-loading-icon-name': 'i-lucide-refresh-ccw', // TODO: to add
  'table-loading-row': 'data-[loading=true]:border-0 absolute inset-x-0 -mt-1.5px',
  'table-loading-cell': '',
  'table-loading': 'absolute inset-x-0 overflow-hidden p-0',

  // table-footer
  'table-footer': 'border-t border-base bg-muted font-medium [&>tr]:last:border-b-0',
}

export const dynamicTable: [RegExp, (params: RegExpExecArray) => string][] = [
]

export const table = [
  ...dynamicTable,
  staticTable,
]

Props

types/table.ts
import type {
  ColumnDef,
  GroupColumnDef,
} from '@tanstack/vue-table'
import type { HTMLAttributes } from 'vue'
import type { NProgressProps } from './progress'

export interface NTableProps<TData, TValue> extends NTableRootProps {
  /**
   * @see https://tanstack.com/table/latest/docs/guide/data
   */
  data: TData[]
  /**
   * @see https://tanstack.com/table/latest/docs/api/core/column
   */
  columns: ColumnDef<TData, TValue>[] | GroupColumnDef<TData, TValue>[]
  /**
   * @see https://tanstack.com/table/latest/docs/api/core/table#getrowid
   */
  rowId?: string
  /**
   * @see https://tanstack.com/table/latest/docs/api/core/table#autoresetall
   */
  autoResetAll?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/row-selection#enablerowselection
   */
  enableRowSelection?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/row-selection#enablemultirowselection
   */
  enableMultiRowSelection?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/row-selection#enablesubrowselection
   */
  enableSubRowSelection?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/column-filtering#enablecolumnfilters
   */
  enableColumnFilters?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#enablesorting
   */
  enableSorting?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#enablemultisort
   */
  enableMultiSort?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#enablemultiremove
   */
  enableMultiRemove?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#enablesortingremoval
   */
  enableSortingRemoval?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#manualsorting
   */
  manualSorting?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#maxmultisortcolcount
   */
  maxMultiSortColCount?: number
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/pagination#manualpagination
   */
  manualPagination?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/pagination#pagecount
   */
  pageCount?: number
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/pagination#rowcount
   */
  rowCount?: number
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/pagination#autoresetpageindex
   */
  autoResetPageIndex?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#sortingfns
   */
  sortingFns?: Record<string, (a: any, b: any) => number>
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#sortdescfirst-1
   */
  sortDescFirst?: boolean
  /**
   * @see https://tanstack.com/table/latest/docs/api/features/sorting#ismultisortevent
   */
  isMultiSortEvent?: (e: unknown) => boolean

  // sub-components props
  _tableHead?: NTableHeadProps
  _tableHeader?: NTableHeaderProps
  _tableFooter?: NTableFooterProps
  _tableBody?: NTableBodyProps
  _tableCaption?: NTableCaptionProps
  _tableRow?: NTableRowProps
  _tableCell?: NTableCellProps
  _tableEmpty?: NTableEmptyProps
  _tableLoading?: NTableLoadingProps

  loading?: boolean

  /**
   * `UnaUI` preset configuration
   *
   * @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/table.ts
   */
  una?: NTableUnaProps
}

export interface NTableRootProps {
  class?: HTMLAttributes['class']

  una?: Pick<NTableUnaProps, 'tableRoot' | 'tableRootWrapper'>
}

export interface NTableBodyProps {
  class?: HTMLAttributes['class']

  una?: Pick<NTableUnaProps, 'tableBody'>
}

export interface NTableHeadProps {
  class?: HTMLAttributes['class']

  dataPinned?: 'left' | 'right' | false

  una?: Pick<NTableUnaProps, 'tableHead'>
}

export interface NTableHeaderProps {
  class?: HTMLAttributes['class']

  una?: Pick<NTableUnaProps, 'tableHeader'>
}

export interface NTableFooterProps {
  class?: HTMLAttributes['class']

  una?: Pick<NTableUnaProps, 'tableFooter'>
}

export interface NTableRowProps {
  class?: HTMLAttributes['class']

  una?: Pick<NTableUnaProps, 'tableRow'>
}

export interface NTableCellProps {
  class?: HTMLAttributes['class']

  dataPinned?: 'left' | 'right' | false

  una?: Pick<NTableUnaProps, 'tableCell'>
}

export interface NTableEmptyProps {
  class?: HTMLAttributes['class']
  colspan?: number

  _tableCell?: NTableCellProps
  _tableRow?: NTableRowProps

  una?: Pick<NTableUnaProps, 'tableEmpty' | 'tableRow' | 'tableCell'>
}

export interface NTableLoadingProps {
  size?: HTMLAttributes['class']
  enabled?: boolean
  class?: HTMLAttributes['class']
  colspan?: number

  _tableCell?: NTableCellProps
  _tableRow?: NTableRowProps
  _tableProgress?: NProgressProps

  una?: Pick<NTableUnaProps, 'tableLoading' | 'tableRow' | 'tableCell'>
}

export interface NTableCaptionProps {
  class?: HTMLAttributes['class']

  una?: Pick<NTableUnaProps, 'tableCaption'>
}

interface NTableUnaProps {
  tableRoot?: HTMLAttributes['class']
  tableRootWrapper?: HTMLAttributes['class']
  tableBody?: HTMLAttributes['class']
  tableHead?: HTMLAttributes['class']
  tableHeader?: HTMLAttributes['class']
  tableFooter?: HTMLAttributes['class']
  tableRow?: HTMLAttributes['class']
  tableCell?: HTMLAttributes['class']
  tableCaption?: HTMLAttributes['class']
  tableEmpty?: HTMLAttributes['class']
  tableLoading?: HTMLAttributes['class']
}

Components

Table.vue
TableRoot.vue
TableHeader.vue
TableHead.vue
TableBody.vue
TableFooter.vue
TableCell.vue
TableRow.vue
TableEmpty.vue
TableLoading.vue
TableCaption.vue
<script setup lang="ts" generic="TData, TValue">
import type {
  ColumnFiltersState,
  ColumnOrderState,
  ColumnPinningState,
  ExpandedState,
  GroupingState,
  Header,
  PaginationState,
  SortingState,
  VisibilityState,
} from '@tanstack/vue-table'
import type { NTableProps } from '../../../types'

import {
  FlexRender,
  getCoreRowModel,
  getExpandedRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useVueTable,
} from '@tanstack/vue-table'

import { computed, h } from 'vue'

import { cn, valueUpdater } from '../../../utils'
import Button from '../../elements/Button.vue'
import Checkbox from '../../forms/Checkbox.vue'
import Input from '../../forms/Input.vue'
import TableBody from './TableBody.vue'
import TableCell from './TableCell.vue'
import TableEmpty from './TableEmpty.vue'
import TableFooter from './TableFooter.vue'
import TableHead from './TableHead.vue'
import TableHeader from './TableHeader.vue'
import TableLoading from './TableLoading.vue'
import TableRoot from './TableRoot.vue'
import TableRow from './TableRow.vue'

const props = withDefaults(defineProps <NTableProps<TData, TValue>>(), {
  enableMultiRowSelection: true,
})

const emit = defineEmits(['select', 'selectAll', 'expand'])

const slots = defineSlots()

const rowSelection = defineModel<Record<string, boolean>>('modelValue')
const sorting = defineModel<SortingState>('sorting')
const columnVisibility = defineModel<VisibilityState>('columnVisibility')
const columnFilters = defineModel<ColumnFiltersState>('columnFilters')
const globalFilter = defineModel<string>('globalFilter')
const columnOrder = defineModel<ColumnOrderState>('columnOrder')
const columnPinning = defineModel<ColumnPinningState>('columnPinning')
const expanded = defineModel<ExpandedState>('expanded')
const grouping = defineModel<GroupingState>('grouping')
const pagination = defineModel<PaginationState>('pagination', {
  default: () => ({
    pageIndex: 0,
    pageSize: 10,
  }),
})

const columnsWithMisc = computed(() => {
  let data = []

  // add selection column
  data = props.enableRowSelection
    ? [
        {
          accessorKey: 'selection',
          header: props.enableMultiRowSelection
            ? ({ table }: any) => h(Checkbox, {
                'modelValue': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
                'onUpdate:modelValue': (value: boolean | 'indeterminate' | null) => {
                  table.toggleAllPageRowsSelected(!!value)
                  emit('selectAll', table.getRowModel().rows)
                },
                'areaLabel': 'Select all rows',
              })
            : '',
          cell: ({ row }: any) => h(Checkbox, {
            'modelValue': row.getIsSelected() ?? false,
            'onUpdate:modelValue': (value: boolean | 'indeterminate' | null) => {
              row.toggleSelected(!!value)
              emit('select', row)
            },
            'areaLabel': 'Select row',
          }),
          enableSorting: false,
          enableHiding: false,
        },
        ...props.columns,
      ]
    : props.columns

  // add expanded column
  data = slots.expanded
    ? [
        {
          accessorKey: 'expanded',
          header: '',
          cell: ({ row }: any) => h(Button, {
            size: 'xs',
            icon: true,
            square: true,
            btn: 'ghost-gray',
            label: 'i-radix-icons-chevron-down',
            onClick: () => {
              row.toggleExpanded()
              emit('expand', row)
            },
            una: {
              btnIconLabel: cn(
                'transform transition-transform duration-200',
                row.getIsExpanded() ? '-rotate-180' : 'rotate-0',
              ),
            },
          }),
          enableSorting: false,
          enableHiding: false,
        },
        ...data,
      ]
    : data

  return data
})

const table = useVueTable({
  get data() {
    return props.data ?? []
  },
  get columns() {
    return columnsWithMisc.value ?? []
  },
  state: {
    get sorting() { return sorting.value },
    get columnFilters() { return columnFilters.value },
    get globalFilter() { return globalFilter.value },
    get rowSelection() { return rowSelection.value },
    get columnVisibility() { return columnVisibility.value },
    get pagination() { return pagination.value },
    get columnOrder() { return columnOrder.value },
    get columnPinning() { return columnPinning.value },
    get expanded() { return expanded.value },
    get grouping() { return grouping.value },
  },

  enableMultiRowSelection: props.enableMultiRowSelection,
  enableSubRowSelection: props.enableSubRowSelection,
  autoResetAll: props.autoResetAll,
  enableRowSelection: props.enableRowSelection,
  enableColumnFilters: props.enableColumnFilters,
  manualPagination: props.manualPagination,
  manualSorting: props.manualSorting,
  pageCount: props.pageCount,
  rowCount: props.rowCount,
  autoResetPageIndex: props.autoResetPageIndex,
  enableSorting: props.enableSorting,
  enableSortingRemoval: props.enableSortingRemoval,
  enableMultiSort: props.enableMultiSort,
  enableMultiRemove: props.enableMultiRemove,
  maxMultiSortColCount: props.maxMultiSortColCount,
  sortingFns: props.sortingFns,
  isMultiSortEvent: props.isMultiSortEvent,

  getCoreRowModel: getCoreRowModel(),
  getSortedRowModel: getSortedRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  getRowId: (row: any) => props.rowId ? row[props.rowId] : row.id,
  getExpandedRowModel: getExpandedRowModel(),

  onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
  onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
  onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
  onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
  onGlobalFilterChange: updaterOrValue => valueUpdater(updaterOrValue, globalFilter),
  onPaginationChange: updaterOrValue => valueUpdater(updaterOrValue, pagination),
  onColumnOrderChange: updaterOrValue => valueUpdater(updaterOrValue, columnOrder),
  onColumnPinningChange: updaterOrValue => valueUpdater(updaterOrValue, columnPinning),
  onExpandedChange: updaterOrValue => valueUpdater(updaterOrValue, expanded),
  onGroupingChange: updaterOrValue => valueUpdater(updaterOrValue, grouping),
})

function getHeaderColumnFiltersCount(headers: Header<unknown, unknown>[]): number {
  let count = 0
  headers.forEach((header) => {
    if (header.column.columnDef.enableColumnFilter)
      count++
  })

  return count
}

defineExpose({
  ...table,
})
</script>

<template>
  <TableRoot
    :class="props.class"
    :una
  >
    <!-- header -->
    <TableHeader
      :una
      v-bind="props._tableHeader"
    >
      <slot name="header" :table="table">
        <TableRow
          v-for="headerGroup in table.getHeaderGroups()"
          :key="headerGroup.id"
          :una
          v-bind="props._tableRow"
        >
          <!-- headers -->
          <TableHead
            v-for="header in headerGroup.headers"
            :key="header.id"
            :colspan="header.colSpan"
            :data-pinned="header.column.getIsPinned()"
            :una
            v-bind="{ ...props._tableHead, ...header.column.columnDef.meta }"
          >
            <Button
              v-if="header.column.columnDef.enableSorting || (header.column.columnDef.enableSorting !== false && enableSorting)"
              btn="ghost-gray"
              size="sm"
              class="font-normal -ml-1em"
              :una="{
                btnTrailing: 'text-sm',
              }"
              :trailing="header.column.getIsSorted() === 'asc'
                ? 'i-lucide-arrow-up-wide-narrow' : header.column.getIsSorted() === 'desc'
                  ? 'i-lucide-arrow-down-narrow-wide' : 'i-lucide-arrow-up-down'"
              @click="header.column.toggleSorting(
                header.column.getIsSorted() === 'asc' ? undefined : header.column.getIsSorted() !== 'desc',
                enableMultiSort,
              )"
            >
              <slot
                :name="`${header.id}-header`"
                :column="header.column"
              >
                <FlexRender
                  v-if="!header.isPlaceholder"
                  :render="header.column.columnDef.header"
                  :props="header.getContext()"
                />
              </slot>
            </Button>

            <slot
              v-else
              :name="`${header.id}-header`"
              :column="header.column"
            >
              <FlexRender
                v-if="!header.isPlaceholder"
                :render="header.column.columnDef.header"
                :props="header.getContext()"
              />
            </slot>
          </TableHead>
        </TableRow>

        <!-- column filters -->
        <template
          v-for="headerGroup in table.getHeaderGroups()"
          :key="headerGroup.id"
        >
          <TableRow
            v-if="getHeaderColumnFiltersCount(headerGroup.headers) > 0 || enableColumnFilters"
            data-filter="true"
            :una
            v-bind="props._tableRow"
          >
            <TableHead
              v-for="header in headerGroup.headers"
              :key="header.id"
              :una
              :colspan="header.colSpan"
              :data-pinned="header.column.getIsPinned()"
              v-bind="{ ...props._tableHead, ...header.column.columnDef.meta }"
            >
              <slot
                v-if="header.id !== 'selection' && ((header.column.columnDef.enableColumnFilter !== false && enableColumnFilters) || header.column.columnDef.enableColumnFilter)"
                :name="`${header.id}-filter`"
                :column="header.column"
              >
                <Input
                  class="w-auto text-sm text-base"
                  :model-value="header.column.getFilterValue() as string"
                  :placeholder="header.column.columnDef.header"
                  @update:model-value="header.column.setFilterValue($event)"
                />
              </slot>
            </TableHead>
          </TableRow>
        </template>
      </slot>

      <TableLoading
        :enabled="props.loading"
        :una
        v-bind="props._tableLoading"
      >
        <slot name="loading" />
      </TableLoading>
    </TableHeader>

    <!-- body -->
    <TableBody
      :una
      v-bind="props._tableBody"
    >
      <slot name="body" :table="table">
        <template v-if="table.getRowModel().rows?.length">
          <template
            v-for="row in table.getRowModel().rows"
            :key="row.id"
          >
            <TableRow
              :data-state="row.getIsSelected() && 'selected'"
              :una
              v-bind="props._tableRow"
            >
              <slot
                name="row"
                :row="row"
              >
                <!-- rows -->
                <TableCell
                  v-for="cell in row.getVisibleCells()"
                  :key="cell.id"
                  :data-pinned="cell.column.getIsPinned()"
                  :una
                  v-bind="{ ...props._tableCell, ...cell.column.columnDef.meta }"
                >
                  <slot
                    :name="`${cell.column.id}-cell`"
                    :cell="cell"
                  >
                    <FlexRender
                      :render="cell.column.columnDef.cell"
                      :props="cell.getContext()"
                    />
                  </slot>
                </TableCell>
              </slot>
            </TableRow>

            <!-- expanded -->
            <TableRow
              v-if="row.getIsExpanded() && $slots.expanded"
              :una
              v-bind="props._tableRow"
            >
              <TableCell
                :colspan="row.getAllCells().length"
                :una
                v-bind="props._tableCell"
              >
                <slot name="expanded" :row="row" />
              </TableCell>
            </TableRow>
          </template>
        </template>

        <TableEmpty
          v-else
          :colspan="table.getAllLeafColumns().length"
          :una
          v-bind="props._tableEmpty"
        >
          <slot name="empty" />
        </TableEmpty>
      </slot>
    </TableBody>

    <!-- footer -->
    <TableFooter
      v-if="table.getFooterGroups().length > 0"
      :una
      v-bind="props._tableFooter"
    >
      <slot name="footer" :table="table">
        <template
          v-for="footerGroup in table.getFooterGroups()"
          :key="footerGroup.id"
        >
          <TableRow
            v-if="footerGroup.headers.length > 0"
            :una
            v-bind="props._tableRow"
          >
            <template
              v-for="header in footerGroup.headers"
              :key="header.id"
            >
              <TableHead
                v-if="header.column.columnDef.footer"
                :colspan="header.colSpan"
                :una
                v-bind="{ ...props._tableHead, ...header.column.columnDef.meta }"
              >
                <slot :name="`${header.id}-footer`" :column="header.column">
                  <FlexRender
                    v-if="!header.isPlaceholder"
                    :render="header.column.columnDef.footer"
                    :props="header.getContext()"
                  />
                </slot>
              </TableHead>
            </template>
          </TableRow>
        </template>
      </slot>
    </TableFooter>
  </TableRoot>
</template>