| name | n-data-table |
| description | A powerful data table component for displaying structured tabular data with features like sorting, filtering, pagination, selection, tree data, virtual scrolling, and more. |
| author | jiaiyan |
| version | 1.0.0 |
n-data-table Component
The n-data-table component is Naive UI's comprehensive solution for displaying structured tabular data. It provides extensive features for data grids, tables, and tabular data display.
When to Use
Use n-data-table when you need to:
- Display structured data in rows and columns
- Create data grids with sorting, filtering, and pagination
- Show tabular data with selection capabilities
- Render tree-structured hierarchical data
- Handle large datasets with virtual scrolling
- Build editable data tables
- Export data to CSV format
Basic Usage
Simple Data Table
<template>
<n-data-table
:columns="columns"
:data="data"
:pagination="pagination"
:bordered="false"
/>
</template>
<script setup>
import { ref } from 'vue';
const columns = [
{ title: 'Name', key: 'name' },
{ title: 'Age', key: 'age' },
{ title: 'Address', key: 'address' },
];
const data = ref([
{ name: 'John Brown', age: 32, address: 'New York No. 1 Lake Park' },
{ name: 'Jim Green', age: 42, address: 'London No. 1 Lake Park' },
{ name: 'Joe Black', age: 38, address: 'Sidney No. 1 Lake Park' },
]);
const pagination = ref({
pageSize: 10,
});
</script>
Data Table with Selection
<template>
<n-data-table
:columns="columns"
:data="data"
:row-key="rowKey"
@update:checked-row-keys="handleCheck"
/>
</template>
<script setup>
import { ref } from 'vue';
const rowKey = row => row.id;
const columns = [
{ type: 'selection' },
{ title: 'Name', key: 'name' },
{ title: 'Age', key: 'age' },
];
const data = ref([
{ id: 1, name: 'John Brown', age: 32 },
{ id: 2, name: 'Jim Green', age: 42 },
]);
const handleCheck = keys => {
console.log('Selected rows:', keys);
};
</script>
Async Data Table
<template>
<n-data-table
remote
:columns="columns"
:data="data"
:loading="loading"
:pagination="pagination"
@update:page="handlePageChange"
@update:sorter="handleSorterChange"
@update:filters="handleFiltersChange"
/>
</template>
<script setup>
import { ref, reactive } from 'vue';
const loading = ref(false);
const data = ref([]);
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
});
const handlePageChange = async page => {
loading.value = true;
pagination.page = page;
await fetchData();
loading.value = false;
};
const fetchData = async () => {
// Fetch data from server
};
</script>
API Reference
Props
| Name | Type | Default | Description |
|---|
allow-checking-not-loaded | boolean | false | Whether to allow cascade checking on not loaded nodes in tree data |
bordered | boolean | true | Whether to show border |
bottom-bordered | boolean | true | Whether to show bottom border |
cascade | boolean | true | Whether to do cascade checking when using tree data |
checked-row-keys | Array<string | number> | undefined | The keys of checked rows (controlled) |
children-key | string | 'children' | The key of children data in tree data's data entity |
columns | Array<DataTableColumn> | [] | Columns to display |
data | Array<object> | [] | Data to display |
default-checked-row-keys | Array<string | number> | [] | Default checked row keys (uncontrolled) |
default-expand-all | boolean | false | Whether to expand all expandable rows |
default-expanded-row-keys | Array<string | number> | [] | Default expanded row keys (uncontrolled) |
expanded-row-keys | Array<string | number> | undefined | Expanded row keys (controlled) |
filter-icon-popover-props | PopoverProps | { trigger: click, placement: bottom } | Filter icon's Popover attributes |
flex-height | boolean | false | Whether to make table body height auto fit table area |
get-csv-cell | (value, row, col) => string | undefined | Custom function to get CSV cell content |
get-csv-header | (cols) => string | undefined | Custom function to get CSV header content |
header-height | number | 28 | Header height when virtual-scroll-header is enabled |
height-for-row | (rowData, index) => number | undefined | Height configuration function for each row |
indent | number | 16 | Indent of row content when using tree data |
loading | boolean | false | Whether to display loading status |
max-height | number | string | undefined | Max height of the table content |
min-height | number | string | undefined | Min height of the table content |
min-row-height | number | 28 | Min row height when virtual-scroll is enabled |
paginate-single-page | boolean | true | Whether to show pagination when data is less than one page |
pagination | false | object | false | Pagination configuration or false to disable |
pagination-behavior-on-filter | 'first' | 'current' | 'current' | Pagination behavior after filter state changes |
remote | boolean | false | Whether to use remote data (disables automatic paging) |
render-cell | (value, rowData, column) => VNodeChild | undefined | Custom render function for all cells |
render-expand-icon | ({ expanded, rowData }) => VNodeChild | undefined | Custom render function for expand icon |
row-class-name | string | (rowData, rowIndex) => string | undefined | Class name for each row |
row-key | (rowData) => number | string | undefined | Function to generate unique row key |
row-props | (rowData, rowIndex) => HTMLAttributes | undefined | Customize row HTML attributes |
scroll-x | number | string | undefined | Horizontal scroll width (required for fixed columns) |
scrollbar-props | ScrollbarProps | undefined | Scrollbar component props |
single-column | boolean | false | Whether rows are not divided (no border-bottom) |
single-line | boolean | true | Whether columns are not divided (no border-right) |
size | 'small' | 'medium' | 'large' | 'medium' | Table size |
spin-props | object | undefined | Loading spin props |
sticky-expanded-rows | boolean | false | Whether expanded row content remains sticky |
striped | boolean | false | Whether to show zebra stripes on rows |
summary | DataTableCreateSummary | undefined | Summary row data configuration |
summary-placement | 'top' | 'bottom' | 'bottom' | Summary rows placement |
table-layout | 'auto' | 'fixed' | 'auto' | CSS table-layout property |
virtual-scroll | boolean | false | Whether to use virtual scrolling for large data |
virtual-scroll-header | boolean | false | Whether to use virtual scrolling in header |
virtual-scroll-x | boolean | false | Whether to use horizontal virtual scrolling |
Column Properties
| Name | Type | Default | Description |
|---|
align | 'left' | 'right' | 'center' | 'left' | Text alignment in column |
allowExport | boolean | true | Whether the column can be exported |
cellProps | (rowData, rowIndex) => object | undefined | HTML attributes for the cell |
children | DataTableColumn[] | undefined | Child columns for grouped header |
className | string | undefined | Class name for the column |
colSpan | (rowData, rowIndex) => number | undefined | Column span of the cell |
customNextSortOrder | (order) => order | undefined | Custom next sorting status function |
defaultFilterOptionValue | string | number | null | null | Default filter value (uncontrolled, single) |
defaultFilterOptionValues | Array<string | number> | [] | Default filter values (uncontrolled, multiple) |
defaultSortOrder | 'descend' | 'ascend' | false | false | Default sort order (uncontrolled) |
disabled | (rowData) => boolean | () => false | Whether the row is checkable |
ellipsis | boolean | EllipsisProps | false | Ellipsis options for overflow content |
ellipsis-component | 'ellipsis' | 'performant-ellipsis' | 'ellipsis' | Component for rendering text ellipsis |
expandable | (rowData) => boolean | undefined | Whether the row is expandable |
filter | boolean | function | false | Filter function or true for async filter |
filterMode | 'and' | 'or' | 'or' | Filter mode for multiple values |
filterMultiple | boolean | true | Whether multiple filter values allowed |
filterOptionValue | string | number | null | undefined | Active filter value (controlled, single) |
filterOptionValues | Array<string | number> | null | undefined | Active filter values (controlled, multiple) |
filterOptions | Array<{ label, value }> | undefined | Filter options |
fixed | 'left' | 'right' | false | false | Whether the column is fixed |
key | string | number | undefined | Unique key of the column |
maxWidth | number | string | undefined | Max width (when resizable is true) |
minWidth | number | string | undefined | Min width of the column |
multiple | boolean | true | Whether multiple selection mode |
options | Array | undefined | Custom selection options |
render | (rowData, rowIndex) => VNodeChild | undefined | Custom cell render function |
renderExpand | (rowData, rowIndex) => VNodeChild | undefined | Render function for expand area |
renderFilter | (options) => VNodeChild | undefined | Custom filter trigger render |
renderFilterIcon | (options) => VNodeChild | undefined | Custom filter icon render |
renderFilterMenu | (actions) => VNodeChild | undefined | Custom filter menu render |
renderSorter | (options) => VNodeChild | undefined | Custom sorter trigger render |
renderSorterIcon | (options) => VNodeChild | undefined | Custom sorter icon render |
resizable | boolean | undefined | Whether column width is resizable |
rowSpan | (rowData, rowIndex) => number | undefined | Row span of the cell |
sortOrder | 'descend' | 'ascend' | false | undefined | Sort order (controlled) |
sorter | boolean | function | 'default' | false | Sorter function or 'default' for built-in |
title | string | (() => VNodeChild) | undefined | Column title |
titleAlign | 'left' | 'right' | 'center' | null | Header text alignment |
titleColSpan | number | undefined | Column span for the title |
tree | boolean | false | Whether to show tree expand trigger |
type | 'selection' | 'expand' | undefined | Column type |
width | number | string | undefined | Width of the column |
Events
| Name | Parameters | Description |
|---|
update:checked-row-keys | (keys, rows, meta) | Emitted when checked row keys change |
update:expanded-row-keys | (keys) | Emitted when expanded row keys change |
update:filters | (filters, initiatorColumn) | Emitted when filters change |
update:page | (page) | Emitted when page changes |
update:page-size | (pageSize) | Emitted when page size changes |
update:sorter | (sortState) | Emitted when sorter changes |
Slots
| Name | Parameters | Description |
|---|
empty | - | Custom content when data is empty |
loading | - | Custom content when loading |
Methods
| Name | Type | Description |
|---|
clearFilters | () => void | Clear all filter state |
clearSorter | () => void | Clear all sort state |
downloadCsv | (options?) => void | Download data as CSV file |
filters | (filters) => void | Set active filters |
page | (page) => void | Manually set the page |
scrollTo | (options) => void | Scroll to specific position |
sort | (columnKey, order) => void | Set sort state |
Common Patterns
Pagination
<template>
<n-data-table :columns="columns" :data="data" :pagination="pagination" />
</template>
<script setup>
import { ref, reactive } from 'vue';
const pagination = reactive({
page: 1,
pageSize: 10,
showSizePicker: true,
pageSizes: [10, 20, 50],
onChange: page => {
pagination.page = page;
},
onUpdatePageSize: pageSize => {
pagination.pageSize = pageSize;
pagination.page = 1;
},
});
</script>
Sorting
<template>
<n-data-table
:columns="columns"
:data="data"
@update:sorter="handleSorterChange"
/>
</template>
<script setup>
const columns = [
{
title: 'Name',
key: 'name',
sorter: (row1, row2) => row1.name.localeCompare(row2.name),
},
{
title: 'Age',
key: 'age',
sorter: (row1, row2) => row1.age - row2.age,
},
];
const handleSorterChange = sortState => {
console.log('Sort state:', sortState);
};
</script>
Filtering
<template>
<n-data-table
:columns="columns"
:data="data"
@update:filters="handleFiltersChange"
/>
</template>
<script setup>
const columns = [
{
title: 'Name',
key: 'name',
},
{
title: 'Address',
key: 'address',
filterOptions: [
{ label: 'London', value: 'London' },
{ label: 'New York', value: 'New York' },
],
filter: (value, row) => row.address.includes(value),
},
];
const handleFiltersChange = filters => {
console.log('Filters:', filters);
};
</script>
Row Selection
<template>
<n-data-table
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
:row-key="rowKey"
/>
</template>
<script setup>
import { ref } from 'vue';
const checkedRowKeys = ref([]);
const rowKey = row => row.id;
const columns = [
{ type: 'selection' },
{ title: 'Name', key: 'name' },
{ title: 'Age', key: 'age' },
];
</script>
Tree Data
<template>
<n-data-table
:columns="columns"
:data="data"
:row-key="rowKey"
default-expand-all
/>
</template>
<script setup>
const columns = [
{ title: 'Name', key: 'name', tree: true },
{ title: 'Age', key: 'age' },
];
const data = [
{
id: 1,
name: 'Parent',
age: 40,
children: [
{ id: 2, name: 'Child 1', age: 20 },
{ id: 3, name: 'Child 2', age: 18 },
],
},
];
const rowKey = row => row.id;
</script>
Virtual Scrolling for Large Data
<template>
<n-data-table
:columns="columns"
:data="largeData"
:max-height="400"
virtual-scroll
/>
</template>
<script setup>
const largeData = Array.from({ length: 10000 }, (_, i) => ({
id: i + 1,
name: `User ${i + 1}`,
age: Math.floor(Math.random() * 50) + 18,
}));
</script>
Fixed Columns
<template>
<n-data-table
:columns="columns"
:data="data"
:scroll-x="1200"
:max-height="300"
/>
</template>
<script setup>
const columns = [
{ title: 'ID', key: 'id', fixed: 'left', width: 100 },
{ title: 'Name', key: 'name', width: 200 },
{ title: 'Age', key: 'age', width: 100 },
{ title: 'Address', key: 'address', width: 300 },
{ title: 'Actions', key: 'actions', fixed: 'right', width: 150 },
];
</script>
Expandable Rows
<template>
<n-data-table :columns="columns" :data="data" />
</template>
<script setup>
import { h } from 'vue';
const columns = [
{
type: 'expand',
expandable: row => row.children?.length > 0,
renderExpand: row => {
return h(
'div',
{ style: 'padding: 16px;' },
`Expanded content for ${row.name}`,
);
},
},
{ title: 'Name', key: 'name' },
{ title: 'Age', key: 'age' },
];
</script>
Export CSV
<template>
<n-space vertical>
<n-button @click="exportCsv">Export CSV</n-button>
<n-data-table ref="tableRef" :columns="columns" :data="data" />
</n-space>
</template>
<script setup>
import { ref } from 'vue';
const tableRef = ref(null);
const exportCsv = () => {
tableRef.value?.downloadCsv({ fileName: 'data-export.csv' });
};
</script>
Best Practices
1. Always Define Row Keys
Each row should have a unique key for proper rendering and selection:
<n-data-table :row-key="row => row.id" :columns="columns" :data="data" />
2. Use Controlled Mode for Async Operations
When fetching data from a server, use controlled mode:
<n-data-table
remote
:loading="loading"
:data="serverData"
:pagination="{
page: currentPage,
pageSize: pageSize,
itemCount: totalItems,
}"
@update:page="handlePageChange"
/>
3. Optimize Large Datasets with Virtual Scrolling
For datasets with thousands of rows, enable virtual scrolling:
<n-data-table :data="largeData" :max-height="400" virtual-scroll />
4. Set Column Widths for Fixed Columns
When using fixed columns, always set explicit widths:
const columns = [
{ title: 'ID', key: 'id', fixed: 'left', width: 80 },
];
5. Use Pagination for Better Performance
Avoid rendering all data at once:
const pagination = {
pageSize: 20,
showSizePicker: true,
pageSizes: [10, 20, 50, 100],
};
6. Handle Empty States
Provide meaningful empty state feedback:
<n-data-table :columns="columns" :data="data">
<template #empty>
<n-empty description="No data available" />
</template>
</n-data-table>
7. Use Custom Render Functions for Complex Cells
For complex cell content, use render functions:
const columns = [
{
title: 'Status',
key: 'status',
render: row => {
return h(
NTag,
{ type: row.status === 'active' ? 'success' : 'warning' },
() => row.status,
);
},
},
];
8. Avoid Deep Nesting in Tree Data
Keep tree data structures reasonably shallow for better performance.
9. Use table-layout: fixed for Consistent Column Widths
When precise column widths are needed:
<n-data-table :columns="columns" :data="data" table-layout="fixed" />
10. Clean Up Filters and Sorters
Provide clear actions for users to reset table state:
<n-space>
<n-button @click="tableRef?.clearFilters()">Clear Filters</n-button>
<n-button @click="tableRef?.clearSorter()">Clear Sorter</n-button>
</n-space>