DataTable
DataTable is a headless, generic table built on @tanstack/vue-table v9, styled with the same primitives as Table. You describe your rows with column definitions, and it renders sortable headers, an optional expansion row, and an empty state.
<script setup lang="ts">
import { createColumnHelper } from '@tanstack/vue-table'
import { DataTable, type DataTableFeatures } from '@myghf/ui'
const data = [
{ id: 'r1', patient: 'Amina Farouk', waiting: 3 },
]
const columnHelper = createColumnHelper<DataTableFeatures, (typeof data)[number]>()
const columns = columnHelper.columns([
columnHelper.accessor('patient', { header: 'Patient' }),
columnHelper.accessor('waiting', { header: 'Waiting (days)' }),
])
const getRowId = (row: (typeof data)[number]) => row.id
</script>
<template>
<DataTable :data="data" :columns="columns" :get-row-id="getRowId" />
</template>Examples
Sorting
Click a header to sort by that column; click again to reverse. Sorting state lives in the component and is reported through update:sorting. Set sortable to false to make every header static.
| Amina Farouk | Cardiology | 3 |
| Youssef Kamal | Radiology | 12 |
| Layla Hassan | Oncology | 1 |
| Omar Said | Cardiology | 7 |
Sorted by: the original order
<script setup lang="ts">
import { ref } from 'vue'
import { createColumnHelper } from '@tanstack/vue-table'
import { DataTable, type DataTableFeatures } from '@myghf/ui'
interface Referral {
id: string
patient: string
clinic: string
waiting: number
}
const referrals: Referral[] = [
{ id: 'r1', patient: 'Amina Farouk', clinic: 'Cardiology', waiting: 3 },
{ id: 'r2', patient: 'Youssef Kamal', clinic: 'Radiology', waiting: 12 },
{ id: 'r3', patient: 'Layla Hassan', clinic: 'Oncology', waiting: 1 },
{ id: 'r4', patient: 'Omar Said', clinic: 'Cardiology', waiting: 7 },
]
const columnHelper = createColumnHelper<DataTableFeatures, Referral>()
const columns = columnHelper.columns([
columnHelper.accessor('patient', { header: 'Patient' }),
columnHelper.accessor('clinic', { header: 'Clinic' }),
columnHelper.accessor('waiting', { header: 'Waiting (days)' }),
])
const getRowId = (row: Referral) => row.id
const sorting = ref<{ id: string; desc: boolean }[]>([])
</script>
<template>
<div class="space-y-3">
<DataTable
:data="referrals"
:columns="columns"
:get-row-id="getRowId"
:sortable="true"
@update:sorting="sorting = $event"
/>
<p class="text-sm text-muted">
Sorted by:
{{
sorting.length
? sorting.map((s) => `${s.id} ${s.desc ? 'descending' : 'ascending'}`).join(', ')
: 'the original order'
}}
</p>
</div>
</template>Expanding rows and cell slots
With expandable, clicking a row toggles an expansion row rendered through the expansion slot. Expansion is single-row (opening one closes the other). Use a cell-<columnId> slot to override a column's cell — here the status column renders a Tag.
| Amina Farouk | Ready |
| Youssef Kamal | Pending |
| Layla Hassan | Ready |
<script setup lang="ts">
import { ref } from 'vue'
import { createColumnHelper } from '@tanstack/vue-table'
import { DataTable, Tag, type DataTableFeatures } from '@myghf/ui'
interface Order {
id: string
patient: string
status: 'Ready' | 'Pending'
notes: string
}
const orders: Order[] = [
{ id: 'o1', patient: 'Amina Farouk', status: 'Ready', notes: 'Consent signed; slot booked for Thursday.' },
{ id: 'o2', patient: 'Youssef Kamal', status: 'Pending', notes: 'Awaiting the radiology report.' },
{ id: 'o3', patient: 'Layla Hassan', status: 'Ready', notes: 'Pre-op bloods completed.' },
]
const columnHelper = createColumnHelper<DataTableFeatures, Order>()
const columns = columnHelper.columns([
columnHelper.accessor('patient', { header: 'Patient' }),
columnHelper.accessor('status', { header: 'Status' }),
])
const getRowId = (row: Order) => row.id
const expanded = ref<Record<string, boolean>>({})
</script>
<template>
<DataTable
v-model:expanded="expanded"
:data="orders"
:columns="columns"
:get-row-id="getRowId"
expandable
empty-label="No orders"
>
<template #cell-status="{ value }">
<Tag :tone="value === 'Ready' ? 'success' : 'warning'">{{ value }}</Tag>
</template>
<template #expansion="{ row }">
<p class="text-sm text-muted">{{ row.original.notes }}</p>
</template>
</DataTable>
</template>Types
DataTableFeatures
columns is typed against DataTableFeatures, the feature set registered in src/lib/table.ts and re-exported from @myghf/ui. It enables exactly three TanStack features:
| Feature | What it adds |
|---|---|
rowSortingFeature (+ sortedRowModel, sortFns) | Sortable columns and the update:sorting state. |
rowPaginationFeature (+ paginatedRowModel) | Registers the paginated row model. DataTable itself renders no pagination controls and does not expose the table instance, so reach this model by composing TanStack directly with DataTableFeatures. |
rowExpandingFeature (+ expandedRowModel) | Per-row expansion driven by the expanded map. |
TanStack v9 tree-shakes anything not registered, so features outside this list — row selection, column filtering, column resizing, grouping — are not available through DataTable. Use it when you need exactly this set; otherwise compose Table yourself.
columns has the type ColumnDef<typeof dataTableFeatures, T>[], so author them with createColumnHelper<DataTableFeatures, T>() from @tanstack/vue-table. A column's numeric size is read structurally and used as the column (header) width.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
data | T[] | — (required) | Row data. |
columns | ColumnDef<DataTableFeatures, T>[] | — (required) | Column definitions. |
getRowId | (row: T) => string | — | Stable row id, used for keys and the expansion map. Strongly recommended. |
striped | boolean | false | Shades every even body row. |
size | 'sm' | 'default' | 'default' | Compact or default text size. |
emptyLabel | string | 'No results' | Message shown when there are no rows. |
sortable | boolean | true | Renders clicking/sorting headers for columns that can sort. |
expandable | boolean | false | Makes rows toggle a detail row. |
expanded | Record<string, boolean> | {} | Expansion map keyed by row id. Bind with v-model:expanded. |
stopRowToggleOnInteractiveCells | boolean | true | Keeps clicks on buttons/links/inputs from also toggling the row. |
maxHeight | string | — | When set, makes the wrapper scroll vertically at this height. |
Events
| Event | Payload | Description |
|---|---|---|
update:sorting | { id: string; desc: boolean }[] | Emitted whenever the sort state changes. |
update:expanded | Record<string, boolean> | Emitted with the new expansion map. |
row-click | unknown | Emitted with the clicked row's original data. Fires even when expandable is false. |
Slots
| Slot | Props | Description |
|---|---|---|
empty | — | Replaces emptyLabel when there are no rows. |
cell-<columnId> | { row, cell, value } | Overrides that column's cell. The column id is its accessor key (or id). |
expansion | { row } | Content of the expansion row, shown for expanded rows only. |
DataTable renders no default slot — all cells come from FlexRender or a cell-* slot.
Exposed methods
None. DataTable does not call defineExpose. Sort and expand through v-model bindings and the events above.
Accessibility
- Sortable headers are real
<button>s inside<th>, with thearia-sortstate written by the underlying TableHead. Non-sortable headers render plain text. Keyboard users can focus and activate each sort button. - Expansion is toggled by clicking the row (
TableRowgetsclickableanddata-state="expanded").stopRowToggleOnInteractiveCellsprevents a click on an interactive cell from also toggling the row. Known gap: the<tr>itself has notabindex, role, or keyboard handler, so row expansion is pointer-only — provide an explicit control (or keep essential detail outside the expansion) if keyboard users must open rows. - With
expandable, the expansion row spans all columns and is revealed inline after the row it belongs to. - The empty state renders a full-width
TableEmptyrow, so the table keeps its structure when there is no data.
Dark mode & RTL
DataTablerenders through Table, so it inherits the same semantic tokens (bg-surface,border-border,bg-surface-muted) and adapts to dark mode. Expansion rows usebg-surface-muted/30.- Sorting icons are Lucide chevrons, and the width is applied per column, so no physical direction is baked in. When
maxHeightis set the wrapper adds anoverflow-autoscroll region; the underlying table's logicaltext-start/text-endalignment mirrors correctly underdir="rtl".