DataGrid
FuiDataGrid is built on @tanstack/vue-table with a bordered rounded container, sortable column headers with always-visible sort icons, row selection footer, and an empty state message.
Basic
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
Sorting
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
Selection
Name | Age | Role | |
|---|---|---|---|
| Alice | 30 | Engineer | |
| Bob | 25 | Designer | |
| Charlie | 35 | PM |
Name | Age | Role | |
|---|---|---|---|
| Alice | 30 | Engineer | |
| Bob | 25 | Designer | |
| Charlie | 35 | PM |
Custom cell rendering
Name | Age | Role | Status | Actions |
|---|---|---|---|---|
| Alice | 30 | Engineer | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | <button style="padding:2px 8px;border:1px solid var(--fui-color-neutral-stroke-1);border-radius:4px;background:var(--fui-color-neutral-background-1);cursor:pointer;font-size:12px;font-family:var(--fui-font-family-base)">Edit</button> |
| Bob | 25 | Designer | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#ff8c00"></span> away </span> | <button style="padding:2px 8px;border:1px solid var(--fui-color-neutral-stroke-1);border-radius:4px;background:var(--fui-color-neutral-background-1);cursor:pointer;font-size:12px;font-family:var(--fui-font-family-base)">Edit</button> |
| Charlie | 35 | PM | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | <button style="padding:2px 8px;border:1px solid var(--fui-color-neutral-stroke-1);border-radius:4px;background:var(--fui-color-neutral-background-1);cursor:pointer;font-size:12px;font-family:var(--fui-font-family-base)">Edit</button> |
| Diana | 28 | Engineer | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#0078d4"></span> vacation </span> | <button style="padding:2px 8px;border:1px solid var(--fui-color-neutral-stroke-1);border-radius:4px;background:var(--fui-color-neutral-background-1);cursor:pointer;font-size:12px;font-family:var(--fui-font-family-base)">Edit</button> |
| Eve | 32 | Designer | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | <button style="padding:2px 8px;border:1px solid var(--fui-color-neutral-stroke-1);border-radius:4px;background:var(--fui-color-neutral-background-1);cursor:pointer;font-size:12px;font-family:var(--fui-font-family-base)">Edit</button> |
| Frank | 40 | Engineer | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#ff8c00"></span> away </span> | <button style="padding:2px 8px;border:1px solid var(--fui-color-neutral-stroke-1);border-radius:4px;background:var(--fui-color-neutral-background-1);cursor:pointer;font-size:12px;font-family:var(--fui-font-family-base)">Edit</button> |
| Grace | 27 | PM | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | <button style="padding:2px 8px;border:1px solid var(--fui-color-neutral-stroke-1);border-radius:4px;background:var(--fui-color-neutral-background-1);cursor:pointer;font-size:12px;font-family:var(--fui-font-family-base)">Edit</button> |
| Henry | 33 | Designer | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#0078d4"></span> vacation </span> | <button style="padding:2px 8px;border:1px solid var(--fui-color-neutral-stroke-1);border-radius:4px;background:var(--fui-color-neutral-background-1);cursor:pointer;font-size:12px;font-family:var(--fui-font-family-base)">Edit</button> |
Filterable
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
Column visibility
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
Pagination
Name | Age | Role |
|---|---|---|
| User 1 | 20 | Engineer |
| User 2 | 21 | Designer |
| User 3 | 22 | PM |
| User 4 | 23 | QA |
| User 5 | 24 | Engineer |
| User 6 | 25 | Designer |
| User 7 | 26 | PM |
| User 8 | 27 | QA |
| User 9 | 28 | Engineer |
| User 10 | 29 | Designer |
Name | Age | Role |
|---|---|---|
| User 1 | 20 | Engineer |
| User 2 | 21 | Designer |
| User 3 | 22 | PM |
| User 4 | 23 | QA |
| User 5 | 24 | Engineer |
Loading
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
Expandable rows
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
Striped rows
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
Sizes
Three density presets control cell padding, header height, selection column width, and font size.
| size | Density | Header height | Body padding (vertical) | Body padding (horizontal) | Selection col width |
|---|---|---|---|---|---|
small | Compact | 36px | 6px | 12px | 36px |
medium | Standard | 48px | 12px | 16px | 48px |
large | Comfortable | 56px | 16px | 20px | 56px |
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
Empty state
Name | Age | Role |
|---|---|---|
| No results. | ||
Full featured
Name | Age | Role | Email | Department | Status | Actions | |
|---|---|---|---|---|---|---|---|
| Alice | 30 | Engineer | alice@example.com | Engineering | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | ||
| Bob | 25 | Designer | bob@example.com | Design | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | ||
| Charlie | 35 | PM | charlie@example.com | Product | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#0078d4"></span> vacation </span> | ||
| Diana | 28 | Engineer | diana@example.com | Engineering | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#ff8c00"></span> away </span> | ||
| Eve | 32 | Designer | eve@example.com | Design | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | ||
| Frank | 40 | Engineer | frank@example.com | Engineering | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | ||
| Grace | 27 | PM | grace@example.com | Product | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#0078d4"></span> vacation </span> | ||
| Henry | 33 | Designer | henry@example.com | Design | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#ff8c00"></span> away </span> | ||
| Ivy | 29 | QA | ivy@example.com | QA | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> | ||
| Jack | 31 | Engineer | jack@example.com | Engineering | <span style="display:inline-flex;align-items:center;gap:4px"> <span style="width:8px;height:8px;border-radius:50%;background:#107c10"></span> active </span> |
Default selection
Name | Age | Role | |
|---|---|---|---|
| Alice | 30 | Engineer | |
| Bob | 25 | Designer | |
| Charlie | 35 | PM | |
| Diana | 28 | Engineer | |
| Eve | 32 | Designer |
Fixed columns
ID | Name | Age | Role | Email | Phone | Department | Location | Start Date | Salary |
|---|---|---|---|---|---|---|---|---|---|
| 1 | User 1 | 20 | Engineer | user1@example.com | 555-0101 | Engineering | New York | 2020-01-15 | 50000 |
| 2 | User 2 | 21 | Designer | user2@example.com | 555-0102 | Design | Seattle | 2021-02-15 | 55000 |
| 3 | User 3 | 22 | PM | user3@example.com | 555-0103 | Product | San Francisco | 2022-03-15 | 60000 |
| 4 | User 4 | 23 | QA | user4@example.com | 555-0104 | QA | Austin | 2023-04-15 | 65000 |
| 5 | User 5 | 24 | Engineer | user5@example.com | 555-0105 | Engineering | New York | 2020-05-15 | 70000 |
| 6 | User 6 | 25 | Designer | user6@example.com | 555-0106 | Design | Seattle | 2021-06-15 | 75000 |
| 7 | User 7 | 26 | PM | user7@example.com | 555-0107 | Product | San Francisco | 2022-07-15 | 80000 |
| 8 | User 8 | 27 | QA | user8@example.com | 555-0108 | QA | Austin | 2023-08-15 | 85000 |
| 9 | User 9 | 28 | Engineer | user9@example.com | 555-0109 | Engineering | New York | 2020-09-15 | 90000 |
| 10 | User 10 | 29 | Designer | user10@example.com | 555-0110 | Design | Seattle | 2021-01-15 | 95000 |
Row borders only
By default, FuiDataGrid applies borders to rows (<tr>) rather than cells (<td>/<th>), giving a cleaner horizontal-line look.
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
Custom header background
Use the header property of a column definition to render custom background colors for individual column headers. The render function uses h() with negative margins to fill the full header cell.
Name | Age | Role | Email |
|---|---|---|---|
| Alice | 30 | Engineer | alice@example.com |
| Bob | 25 | Designer | bob@example.com |
| Charlie | 35 | PM | charlie@example.com |
Column header filter
Set meta.filterable on a column to show a filter icon in its header. Clicking opens a popover with checkboxes (via meta.filterOptions). The component does not filter data locally — it emits @update:filters so you can handle filtering on the server side.
Name | Age | Role |
|---|---|---|
| Alice | 30 | Engineer |
| Bob | 25 | Designer |
| Charlie | 35 | PM |
| Diana | 28 | Engineer |
| Eve | 32 | Designer |
| Frank | 40 | Engineer |
| Grace | 27 | PM |
| Henry | 33 | Designer |
Server-side filtering
Listen to @update:filters to react when the user applies or clears a column filter, then fetch filtered data from your API.
<script setup lang="ts">
const serverData = ref([])
const columns = [
ch.accessor('role', {
header: 'Role',
meta: {
filterable: true,
filterOptions: [
{ label: 'Engineer', value: 'Engineer' },
{ label: 'Designer', value: 'Designer' },
{ label: 'PM', value: 'PM' },
],
},
}),
]
function handleFilters(filters: { id: string; value: any[] }[]) {
// filters = [{ id: 'role', value: ['Engineer', 'PM'] }]
fetchData({ filters }).then(res => { serverData.value = res.data })
}
</script>
<template>
<FuiDataGrid :data="serverData" :columns="columns" @update:filters="handleFilters" />
</template>Interaction
Row Selection
When selectable is enabled, the following interactions are supported:
| Action | selectable="single" | selectable="true" / 'multi' |
|---|---|---|
| Click row | Select this row | Toggle this row, preserve others |
| Ctrl / Cmd + click row | Same as click | Toggle this row, preserve others |
| Shift + click row | Same as click | Select range from last clicked row to current row |
| Click checkbox | Toggle this row | Toggle this row |
Interactive elements inside cells (<button>, <a>, <input>, etc.) do not trigger row selection when clicked.
Fluent 2 design recommends using the checkbox alone to indicate selected rows. Background color changes are an additional visual aid.
Sorting
Click a sortable column header to toggle ascending / descending / unsorted. The active sort column shows a sort arrow icon.
Filtering
- Toolbar filter: When
filterableis enabled, a search input appears in the toolbar for real-time data filtering. - Column header filter: Set
meta.filterableon a column definition to enable a filter popover with multi-select checkboxes.
Pagination
When pagination is enabled, pagination controls appear at the bottom. Use pageSize and pageSizeOptions to customize the page size.
Fixed Columns
Set meta.fixed: 'left' | 'right' on a column to pin it. Fixed columns remain visible during horizontal scroll with shadow indicators. All cells use opaque backgrounds in fixed-column mode to mask scrolled content.
Expandable Rows
When expandable is enabled, use the #expanded-row slot to customize expanded content. Clicking a row does not affect the expanded state.
Props
| Component | Prop | Type | Default | Description |
|---|---|---|---|---|
| ColumnDef | meta.fixed | 'left' | 'right' | — | Pin column to left/right side |
| ColumnDef | meta.filterable | boolean | — | Show header filter icon + popover for this column |
| ColumnDef | meta.filterOptions | { label, value }[] | — | Options shown as checkboxes in the filter popover |
| DataGrid | data | Record<string, any>[] | — | Row data array |
| DataGrid | columns | ColumnDef[] | — | TanStack column definitions |
| DataGrid | sortable | boolean | false | Enable column sorting |
| DataGrid | selectable | boolean | 'single' | 'multi' | false | Enable row selection |
| DataGrid | filterable | boolean | false | Show search filter in toolbar |
| DataGrid | filterColumn | string | '' | Column id to filter by |
| DataGrid | columnVisibility | boolean | false | Show column toggle menu in toolbar |
| DataGrid | pagination | boolean | { pageSize?, pageSizeOptions? } | false | Enable pagination |
| DataGrid | expandable | boolean | false | Enable expandable rows |
| DataGrid | loading | boolean | false | Show loading overlay |
| DataGrid | striped | boolean | false | Alternating row colors |
| DataGrid | size | 'small' | 'medium' | 'large' | 'medium' | Table density preset (compact / standard / comfortable) |
| DataGrid | stickyHeader | boolean | false | Sticky header row |
| DataGrid | emptyMessage | string | 'No results.' | Empty state text |
| DataGrid | rowKey | string | Function | — | Row ID accessor |
| DataGrid | sorting | SortingState | — | v-model for sort state |
| DataGrid | rowSelection | Record<string, boolean> | — | v-model for selection state |
Events
| Event | Payload | Description |
|---|---|---|
| update:filters | { id: string; value: any[] }[] | Emitted when column filter is applied or cleared. For server-side filtering, listen to this event and request filtered data from your API. |
Slots
| Slot | Props | Description |
|---|---|---|
| toolbar | — | Custom toolbar content |
| expanded-row | { row } | Expanded row content |
| empty | — | Custom empty state |
| loading | — | Custom loading content |
Methods
| Method | Params | Description |
|---|---|---|
| downloadCsv | filename?: string | Export data as CSV |
| table | — | TanStack VueTable instance |
Installation
@tanstack/vue-table is bundled as a dependency. No additional install needed — define columns via createColumnHelper and pass to <FuiDataGrid>.
<script setup>
import { createColumnHelper } from '@tanstack/vue-table'
</script>