Skip to content

DataTable

DataTable is a schema-driven table powered by TanStack Table. It wraps Table and renders headers and rows from a column schema and data array.

When to use DataTable vs Table

Use DataTable when you have a columns + data model and want the library to drive headers, rows, client- or server-side sorting and pagination, and built-in selection or expansion. Use Table when you need full control over layout with #head and #body slots (custom header/cell structure, non-tabular rendering, or integration patterns that don’t fit a column schema). See Table for slot-based examples.

Basic usage

Define columns with createColumnHelper and pass them along with your data. DataTable handles the rest.

Product Name
Brand
Category
Price per unit
Strain
Citrus Splash 2x50mg THC Gummies
True North CollectiveEdibles & Ingestibles$2.50
Rabbit Hole 1 lb bag
MJ VerdantFlower$2,800.00Hybrid
THC-A Crystalline Rings | GMO
Kola FarmsConcentrates$10.00
Seed Junky Purple Push Pop 3.5g
Seed JunkyFlower$20.00Hybrid
GUSH-MINTS 19-23% 1.4% TERPENES
AgronomosFlower$1,400.00Indica Hybrid
vue
<script setup lang="ts">
  import { createColumnHelper } from '@packages/vue/src/components/DataTable';
  import DataTable from '@packages/vue/src/components/DataTable/DataTable.vue';
  import { getProducts, ProductFixture } from '@packages/vue/src/components/Table/Table.fixtures';
  import { money } from '@packages/vue/src/utils/i18n';
  import { computed, h } from 'vue';

  const products = computed(() => getProducts().slice(0, 5));

  type Product = ReturnType<typeof getProducts>[number];

  function getRowAccentColor(row: Product) {
    const idx = products.value.findIndex((p) => p.id === row.id);
    return idx === 1 ? 'red-500' : idx === 3 ? 'blue-500' : undefined;
  }

  const columnHelper = createColumnHelper<ProductFixture>();

  const columns = [
    columnHelper.accessor('name', {
      header: 'Product Name',
      cell: (info) =>
        h('div', { class: 'flex items-center' }, [
          h('img', { src: info.row.original.featured_image, width: 60, class: 'rounded mr-3' }),
          info.getValue(),
        ]),
      minSize: 300,
    }),
    columnHelper.accessor((row) => row.brand.name, {
      id: 'brand',
      header: 'Brand',
    }),
    columnHelper.accessor((row) => row.category.name, {
      id: 'category',
      header: 'Category',
    }),
    columnHelper.accessor('price_per_unit', {
      header: 'Price per unit',
      cell: (info) => money(info.getValue()),
    }),
    columnHelper.accessor('strain_classification', {
      header: 'Strain',
      enableSorting: false,
    }),
  ];
</script>

<template>
  <DataTable :columns="columns" :data="products" :get-row-accent-color="getRowAccentColor" />
</template>

The basic example also uses getRowAccentColor to show a row accent on rows 2 and 4.

TanStack Table re-exports

We re-export TanStack Table functions and types from @leaflink/stash/DataTable so downstream apps can build column definitions and work with the table instance without adding @tanstack/vue-table as a direct dependency. Import createColumnHelper, flexRender, and the row-model helpers (getCoreRowModel, getSortedRowModel, etc.) as well as types such as ColumnDef, Row, Table, and TableOptions from @leaflink/stash/DataTable. See the DataTable barrel for the full list of re-exports.

Column definitions

Columns are TanStack Table ColumnDef objects. Use createColumnHelper<TData>() and accessor for typed accessors. Key options:

  • id — Column ID (defaults to accessor key); use for sorting when the key isn’t a string (e.g. (row) => row.brand.name with id: 'brand').
  • header — String or render function for the header.
  • cell — Render function (info) => VNode; info.getValue(), info.row.original, etc.
  • enableSorting — Set to false to disable sorting on that column.
  • minSize, maxSize, size — Column sizing (px); applied as min-width / max-width / width on header and cells.
  • meta.class, meta.headerClass, meta.cellClass — CSS classes for the column’s header or cells.

See TanStack Table’s column API for the full set of options.

Sorting

When DataTable is used standalone, sorting is client-side: click a column header to sort. Disable sorting per column with enableSorting: false. When DataTable is used inside DataView with variant="table", sorting is server-driven: column IDs are matched to DataView sort options and the parent should refetch on @update using DataView’s ordering (see DataView).

Pagination

Standalone DataTable uses client-side pagination. When there is more than one page (getPageCount() > 1), DataTable renders a <Paginate> below the table; page size is controlled by TanStack table state (default 10). When DataTable is inside DataView, pagination is server-driven and the built-in pagination is hidden; use DataView’s toolbar and currentPage / pageSize (see DataView).

Product Name
Brand
Category
Price per unit
Citrus Splash 2x50mg THC Gummies
True North CollectiveEdibles & Ingestibles$2.50
Rabbit Hole 1 lb bag
MJ VerdantFlower$2,800.00
THC-A Crystalline Rings | GMO
Kola FarmsConcentrates$10.00
Seed Junky Purple Push Pop 3.5g
Seed JunkyFlower$20.00
GUSH-MINTS 19-23% 1.4% TERPENES
AgronomosFlower$1,400.00
Dosilato Pre-Rolls (Fully Prepackaged) MEDICAL
New Lyfe MichiganPre-Rolls$5.00
Wonder - Focus - Prickly Pear & Ginger 100mg Gummies
CrescoEdibles & Ingestibles$2.00
200 MG Blue Raspberry Gummies (Gluten Free & Vegan)
IndulgeEdibles & Ingestibles$6.00
Apple Fritter (24.53% THC)(27.123% THCa)
Custom GeneticsFlower$1,200.00
Energy Mist- 150mg THC 150mg CBD edible spray
YouMistEdibles & Ingestibles$7.50
vue
<script setup lang="ts">
  import { createColumnHelper } from '@packages/vue/src/components/DataTable';
  import DataTable from '@packages/vue/src/components/DataTable/DataTable.vue';
  import { getProducts, ProductFixture } from '@packages/vue/src/components/Table/Table.fixtures';
  import { money } from '@packages/vue/src/utils/i18n';
  import { computed, h } from 'vue';

  const products = computed(() => getProducts().slice(0, 25));

  const columnHelper = createColumnHelper<ProductFixture>();

  const columns = [
    columnHelper.accessor('name', {
      header: 'Product Name',
      cell: (info) =>
        h('div', { class: 'flex items-center' }, [
          h('img', { src: info.row.original.featured_image, width: 60, class: 'rounded mr-3' }),
          info.getValue(),
        ]),
      minSize: 300,
    }),
    columnHelper.accessor((row) => row.brand.name, {
      id: 'brand',
      header: 'Brand',
    }),
    columnHelper.accessor((row) => row.category.name, {
      id: 'category',
      header: 'Category',
    }),
    columnHelper.accessor('price_per_unit', {
      header: 'Price per unit',
      cell: (info) => money(info.getValue()),
    }),
  ];
</script>

<template>
  <DataTable :columns="columns" :data="products" />
</template>

Selection

Set is-selectable to add a checkbox column and use TanStack row selection (current page only). Optionally provide get-row-id for stable row IDs (recommended when using selection) and get-row-can-select to disable selection on specific rows. Access selection via a template ref: allPageRowsSelected, somePageRowsSelected, selectedItems, and table (e.g. table.toggleAllPageRowsSelected()). Selection state is owned by the table; clear it when needed (e.g. on pagination) with table.setRowSelection({}).

Selected items: 0
Selected items: []
Product Name
Brand
Category
Price per unit
Strain
Citrus Splash 2x50mg THC GummiesTrue North CollectiveEdibles & Ingestibles$2.50
Rabbit Hole 1 lb bagMJ VerdantFlower$2,800.00Hybrid
THC-A Crystalline Rings | GMOKola FarmsConcentrates$10.00
Seed Junky Purple Push Pop 3.5gSeed JunkyFlower$20.00Hybrid
GUSH-MINTS 19-23% 1.4% TERPENESAgronomosFlower$1,400.00Indica Hybrid
vue
<script setup lang="ts">
  import Box from '@packages/vue/src/components/Box/Box.vue';
  import { createColumnHelper } from '@packages/vue/src/components/DataTable';
  import DataTable from '@packages/vue/src/components/DataTable/DataTable.vue';
  import { getProducts } from '@packages/vue/src/components/Table/Table.fixtures';
  import { money } from '@packages/vue/src/utils/i18n';
  import { computed, useTemplateRef } from 'vue';

  const products = computed(() => getProducts().slice(0, 5));
  const dataTableRef = useTemplateRef('dataTableRef');
  const selectedCount = computed(() => dataTableRef.value?.table.getSelectedRowModel().rows.length ?? 0);
  const selectedItems = computed(
    () => dataTableRef.value?.table.getSelectedRowModel().rows.map((row) => row.original) ?? [],
  );

  type Product = ReturnType<typeof getProducts>[number];
  const columnHelper = createColumnHelper<Product>();

  const columns = [
    columnHelper.accessor('name', {
      header: 'Product Name',
      cell: (info) => info.getValue(),
    }),
    columnHelper.accessor((row) => row.brand.name, {
      id: 'brand',
      header: 'Brand',
    }),
    columnHelper.accessor((row) => row.category.name, {
      id: 'category',
      header: 'Category',
    }),
    columnHelper.accessor('price_per_unit', {
      header: 'Price per unit',
      cell: (info) => money(info.getValue()),
    }),
    columnHelper.accessor('strain_classification', {
      header: 'Strain',
      enableSorting: false,
    }),
  ];
</script>

<template>
  <div class="flex flex-col gap-2">
    <Box>
      Selected items: {{ selectedCount }}<br />
      Selected items: {{ selectedItems.map((item) => item.name) }}
    </Box>
    <DataTable
      ref="dataTableRef"
      :columns="columns"
      :data="products"
      is-selectable
      :get-row-id="(row) => String(row.id)"
      :get-row-can-select="(row) => row.original.id !== 914154"
    />
  </div>
</template>

Expansion

Enable row expansion with is-expandable and get-row-can-expand (return true for rows that can expand). Use the #expansion slot to render custom content; the slot receives { row } (TanStack Row). For hierarchical data, provide get-sub-rows to return child rows; the default expand column then toggles sub-rows. Use hide-expansion-divider to remove the divider between row and expanded content, and has-custom-expand-toggle to hide the default expand column and place the toggle in your own cell.

Product Name
Brand
Category
Price per unit
Citrus Splash 2x50mg THC Gummies
True North CollectiveEdibles & Ingestibles$2.50

Citrus Splash 2x50mg THC Gummies

Citrus Splash 2x50mg THC Gummies

SKU
1234567890
Inventory
100 units
Price unit
Case (100 Units)
Rabbit Hole 1 lb bag
MJ VerdantFlower$2,800.00

Rabbit Hole 1 lb bag

Rabbit Hole 1 lb bag

SKU
1234567890
Inventory
100 units
Price unit
Pound
THC-A Crystalline Rings | GMO
Kola FarmsConcentrates$10.00

THC-A Crystalline Rings | GMO

THC-A Crystalline Rings | GMO

SKU
1234567890
Inventory
100 units
Price unit
Case (12 Units)
Seed Junky Purple Push Pop 3.5g
Seed JunkyFlower$20.00

Seed Junky Purple Push Pop 3.5g

Seed Junky Purple Push Pop 3.5g

SKU
1234567890
Inventory
100 units
Price unit
Case (32 Units)
GUSH-MINTS 19-23% 1.4% TERPENES
AgronomosFlower$1,400.00

GUSH-MINTS 19-23% 1.4% TERPENES

GUSH-MINTS 19-23% 1.4% TERPENES

SKU
1234567890
Inventory
100 units
Price unit
Pound
vue
<script setup lang="ts">
  import { createColumnHelper } from '@packages/vue/src/components/DataTable';
  import DataTable from '@packages/vue/src/components/DataTable/DataTable.vue';
  import { getProducts } from '@packages/vue/src/components/Table/Table.fixtures';
  import { money } from '@packages/vue/src/utils/i18n';
  import { computed, h } from 'vue';

  const products = computed(() => getProducts().slice(0, 5));

  type Product = ReturnType<typeof getProducts>[number];

  const columnHelper = createColumnHelper<Product>();

  const columns = [
    columnHelper.accessor('name', {
      header: 'Product Name',
      cell: (info) =>
        h('div', { class: 'flex items-center' }, [
          h('img', { src: info.row.original.featured_image, width: 60, class: 'rounded mr-3' }),
          info.getValue(),
        ]),
      minSize: 300,
    }),
    columnHelper.accessor((row) => row.brand.name, {
      id: 'brand',
      header: 'Brand',
    }),
    columnHelper.accessor((row) => row.category.name, {
      id: 'category',
      header: 'Category',
    }),
    columnHelper.accessor('price_per_unit', {
      header: 'Price per unit',
      cell: (info) => money(info.getValue()),
    }),
  ];
</script>

<template>
  <DataTable :columns="columns" :data="products" is-expandable :get-row-can-expand="() => true">
    <template #expansion="{ row }">
      <div class="px-6 py-4">
        <p class="font-semibold mb-2">{{ row.original.name }}</p>
        <p class="text-sm text-ice-700">{{ row.original.description }}</p>
        <dl class="mt-3 grid grid-cols-2 gap-x-4 gap-y-1 text-sm">
          <dt class="text-ice-500">SKU</dt>
          <dd>{{ row.original.sku }}</dd>
          <dt class="text-ice-500">Inventory</dt>
          <dd>{{ row.original.inventory }} units</dd>
          <dt class="text-ice-500">Price unit</dt>
          <dd>{{ row.original.display_price_unit }}</dd>
        </dl>
      </div>
    </template>
  </DataTable>
</template>

TIP

The expansion panel is a single full-width cell. Render anything inside: nested tables, forms, charts, or detail panels.

Row accent

get-row-accent-color is a function (row) => StashCommonColor | undefined. When it returns a color, that row gets a left-edge accent bar (same as Table’s row accent). The basic example uses it to highlight rows 2 and 4.

Empty and loading states

DataTable forwards Table’s isEmpty, is-loading, empty-state-text, and #empty slot. Set isEmpty when there is no data to show the empty state; use is-loading for loading; use #empty or empty-state-text to customize the empty message.

Product Name
Brand
Category
Price per unit

No products

Add a product to get started.

vue
<script setup lang="ts">
  import Button from '@packages/vue/src/components/Button/Button.vue';
  import { createColumnHelper } from '@packages/vue/src/components/DataTable';
  import DataTable from '@packages/vue/src/components/DataTable/DataTable.vue';
  import EmptyState from '@packages/vue/src/components/EmptyState/EmptyState.vue';
  import { getProducts, ProductFixture } from '@packages/vue/src/components/Table/Table.fixtures';
  import { money } from '@packages/vue/src/utils/i18n';
  import { computed } from 'vue';

  const products = computed<ReturnType<typeof getProducts>>(() => []);

  const columnHelper = createColumnHelper<ProductFixture>();

  const columns = [
    columnHelper.accessor('name', { header: 'Product Name' }),
    columnHelper.accessor((row) => row.brand.name, { id: 'brand', header: 'Brand' }),
    columnHelper.accessor((row) => row.category.name, { id: 'category', header: 'Category' }),
    columnHelper.accessor('price_per_unit', {
      header: 'Price per unit',
      cell: (info) => money(info.getValue()),
    }),
  ];
</script>

<template>
  <DataTable :columns="columns" :data="products" :is-empty="products.length === 0">
    <template #empty>
      <EmptyState title="No products" subtitle="Add a product to get started." vignette="orders-empty">
        <template #button>
          <Button>Add product</Button>
        </template>
      </EmptyState>
    </template>
  </DataTable>
</template>
Product Name
Brand
Category
Price per unit
vue
<script setup lang="ts">
  import { createColumnHelper } from '@packages/vue/src/components/DataTable';
  import DataTable from '@packages/vue/src/components/DataTable/DataTable.vue';
  import { getProducts, ProductFixture } from '@packages/vue/src/components/Table/Table.fixtures';
  import { money } from '@packages/vue/src/utils/i18n';
  import { computed, h } from 'vue';

  const products = computed(() => getProducts().slice(0, 5));

  const columnHelper = createColumnHelper<ProductFixture>();

  const columns = [
    columnHelper.accessor('name', {
      header: 'Product Name',
      cell: (info) =>
        h('div', { class: 'flex items-center' }, [
          h('img', { src: info.row.original.featured_image, width: 60, class: 'rounded mr-3' }),
          info.getValue(),
        ]),
      minSize: 300,
    }),
    columnHelper.accessor((row) => row.brand.name, {
      id: 'brand',
      header: 'Brand',
    }),
    columnHelper.accessor((row) => row.category.name, {
      id: 'category',
      header: 'Category',
    }),
    columnHelper.accessor('price_per_unit', {
      header: 'Price per unit',
      cell: (info) => money(info.getValue()),
    }),
  ];
</script>

<template>
  <DataTable :columns="columns" :data="products" :is-loading="true" />
</template>

With DataView (server-side)

DataTable works inside DataView with variant="table" for server-driven sorting and pagination. TanStack column IDs are matched to DataView sort options. For filters, toolbar, and pagination behavior, see DataView and Table with DataView patterns.

Product Name
Brand
Category
Price per unit
Strain
No results
vue
<script setup lang="ts">
  import { createColumnHelper } from '@packages/vue/src/components/DataTable';
  import DataTable from '@packages/vue/src/components/DataTable/DataTable.vue';
  import { fetchProducts as apiGetProducts, Product } from '@packages/vue/src/components/DataView/DataView.fixtures';
  import DataView from '@packages/vue/src/components/DataView/DataView.vue';
  import DataViewSortButton from '@packages/vue/src/components/DataViewSortButton/DataViewSortButton.vue';
  import DataViewToolbar from '@packages/vue/src/components/DataViewToolbar/DataViewToolbar.vue';
  import { money } from '@packages/vue/src/utils/i18n';
  import { onMounted, ref, useTemplateRef } from 'vue';

  const PAGE_SIZE = 4;

  const sortOptions = [
    { id: 'product_name', labelAsc: 'Product Name (A-Z)', labelDesc: 'Product Name (Z-A)' },
    { id: 'category_name', labelAsc: 'Category Name (A-Z)', labelDesc: 'Category Name (Z-A)' },
    {
      id: 'price_per_unit_amount',
      labelAsc: 'Price Per Unit (Low to High)',
      labelDesc: 'Price Per Unit (High to Low)',
    },
    { id: 'strain_classification', labelAsc: 'Strain (A-Z)', labelDesc: 'Strain (Z-A)' },
  ];

  const columnHelper = createColumnHelper<Product>();
  const columns = [
    columnHelper.accessor('name', {
      id: 'product_name',
      header: 'Product Name',
      cell: (info) => info.getValue(),
    }),
    columnHelper.accessor((row) => row.brand.name, {
      id: 'brand',
      header: 'Brand',
      enableSorting: false,
    }),
    columnHelper.accessor((row) => row.category.name, {
      id: 'category_name',
      header: 'Category',
    }),
    columnHelper.accessor((row) => row.price_per_unit, {
      id: 'price_per_unit_amount',
      header: 'Price per unit',
      cell: (info) => money(info.getValue()),
    }),
    columnHelper.accessor('strain_classification', {
      id: 'strain_classification',
      header: 'Strain',
    }),
  ];

  const dataViewRef = useTemplateRef('dataViewRef');
  const products = ref<Product[]>([]);
  const isLoadingProducts = ref(false);
  const totalProductCount = ref(0);

  async function fetchProducts() {
    if (!dataViewRef.value || isLoadingProducts.value) {
      return;
    }

    try {
      isLoadingProducts.value = true;

      const response = await apiGetProducts({
        params: {
          page: dataViewRef.value.page || 1,
          pageSize: dataViewRef.value.pageSize || PAGE_SIZE,
          ordering: dataViewRef.value.ordering,
          search: dataViewRef.value.search,
        },
      });

      products.value = response.results;
      totalProductCount.value = response.count;
    } finally {
      isLoadingProducts.value = false;
    }
  }

  onMounted(() => {
    fetchProducts();
  });
</script>

<template>
  <DataView
    ref="dataViewRef"
    variant="table"
    :data="products"
    :page-size="PAGE_SIZE"
    :total-data-count="totalProductCount"
    :is-loading="isLoadingProducts"
    :is-empty="products.length === 0"
    @update="fetchProducts"
  >
    <DataViewToolbar>
      <DataViewSortButton :sort-options="sortOptions" />
    </DataViewToolbar>

    <DataTable :columns="columns" :data="products" />
  </DataView>
</template>

DataView + DataTable with selection

Use is-selectable with a ref to the DataTable and bind the toolbar’s selection props to the exposed allPageRowsSelected, somePageRowsSelected, and selectedItems. Handle the toolbar’s @select with table.toggleAllPageRowsSelected(). Clear selection when data or page changes (e.g. table.setRowSelection({})) so the toolbar stays in sync. See DataView + DataTable with selection for the full example.

Product Name
Brand
Category
Price per unit
Strain
No results
vue
<script setup lang="ts">
  import { createColumnHelper } from '@packages/vue/src/components/DataTable';
  import DataTable from '@packages/vue/src/components/DataTable/DataTable.vue';
  import { fetchProducts as apiGetProducts, Product } from '@packages/vue/src/components/DataView/DataView.fixtures';
  import DataView from '@packages/vue/src/components/DataView/DataView.vue';
  import DataViewSortButton from '@packages/vue/src/components/DataViewSortButton/DataViewSortButton.vue';
  import DataViewToolbar from '@packages/vue/src/components/DataViewToolbar/DataViewToolbar.vue';
  import { money } from '@packages/vue/src/utils/i18n';
  import { computed, onMounted, ref, useTemplateRef } from 'vue';

  const PAGE_SIZE = 4;

  const sortOptions = [
    { id: 'product_name', labelAsc: 'Product Name (A-Z)', labelDesc: 'Product Name (Z-A)' },
    { id: 'category_name', labelAsc: 'Category Name (A-Z)', labelDesc: 'Category Name (Z-A)' },
    {
      id: 'price_per_unit_amount',
      labelAsc: 'Price Per Unit (Low to High)',
      labelDesc: 'Price Per Unit (High to Low)',
    },
    { id: 'strain_classification', labelAsc: 'Strain (A-Z)', labelDesc: 'Strain (Z-A)' },
  ];

  const columnHelper = createColumnHelper<Product>();
  const columns = [
    columnHelper.accessor('name', {
      id: 'product_name',
      header: 'Product Name',
      cell: (info) => info.getValue(),
    }),
    columnHelper.accessor((row) => row.brand.name, {
      id: 'brand',
      header: 'Brand',
      enableSorting: false,
    }),
    columnHelper.accessor((row) => row.category.name, {
      id: 'category_name',
      header: 'Category',
    }),
    columnHelper.accessor((row) => row.price_per_unit, {
      id: 'price_per_unit_amount',
      header: 'Price per unit',
      cell: (info) => money(info.getValue()),
    }),
    columnHelper.accessor('strain_classification', {
      id: 'strain_classification',
      header: 'Strain',
    }),
  ];

  const dataViewRef = useTemplateRef('dataViewRef');
  const dataTableRef = useTemplateRef('dataTableRef');
  const products = ref<Product[]>([]);
  const isLoadingProducts = ref(false);
  const totalProductCount = ref(0);

  const allRowsSelected = computed(() => dataTableRef.value?.allPageRowsSelected);
  const someRowsSelected = computed(() => dataTableRef.value?.somePageRowsSelected);
  const selectedItemsCount = computed(() => dataTableRef.value?.selectedItems?.length ?? 0);

  function onToolbarSelect() {
    dataTableRef.value?.table.toggleAllPageRowsSelected();
  }

  async function fetchProducts() {
    if (!dataViewRef.value || isLoadingProducts.value) {
      return;
    }

    try {
      isLoadingProducts.value = true;

      const response = await apiGetProducts({
        params: {
          page: dataViewRef.value.page || 1,
          pageSize: dataViewRef.value.pageSize || PAGE_SIZE,
          ordering: dataViewRef.value.ordering,
          search: dataViewRef.value.search,
        },
      });

      products.value = response.results;
      totalProductCount.value = response.count;
      dataTableRef.value?.table.setRowSelection({});
    } finally {
      isLoadingProducts.value = false;
    }
  }

  onMounted(() => {
    fetchProducts();
  });
</script>

<template>
  <DataView
    ref="dataViewRef"
    variant="table"
    :data="products"
    :page-size="PAGE_SIZE"
    :total-data-count="totalProductCount"
    :is-loading="isLoadingProducts"
    :is-empty="products.length === 0"
    @update="fetchProducts"
  >
    <DataViewToolbar
      :all-rows-selected="allRowsSelected"
      :some-rows-selected="someRowsSelected"
      :selected-items-count="selectedItemsCount"
      @select="onToolbarSelect"
    >
      <DataViewSortButton :sort-options="sortOptions" />
    </DataViewToolbar>

    <DataTable
      ref="dataTableRef"
      :columns="columns"
      :data="products"
      is-selectable
      :get-row-id="(row) => String(row.id)"
    />
  </DataView>
</template>

useDataTable composable

DataTable is built on the useDataTable composable, which creates the TanStack table instance and switches between client-side and server-side behavior when used inside DataView. The composable is exported from @leaflink/stash/DataTable for advanced cases (e.g. custom table markup with TanStack state). For most use cases, use the DataTable component rather than the composable directly. See useDataTable for details.

API

See the documentation below for a complete reference to the props and APIs of the components used by DataTable.