Table
The Table family is a set of unstyled primitives for building accessible tables from slots: Table, TableHeader, TableBody, TableRow, TableHead, TableCell, and TableEmpty. TablePagination is a companion control that pairs with them. For sorting and expanding, use DataTable.
<script setup lang="ts">
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@myghf/ui'
const rows = [
{ id: 1, name: 'Amina Farouk', clinic: 'Cardiology' },
]
</script>
<template>
<Table striped>
<TableHeader>
<TableRow>
<TableHead>Patient</TableHead>
<TableHead>Clinic</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in rows" :key="row.id">
<TableCell>{{ row.name }}</TableCell>
<TableCell>{{ row.clinic }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template>Examples
Rows, cells, and zones
Compose <thead>/<tbody> through TableHeader/TableBody, and rows/cells through TableRow/TableHead/TableCell. striped shades every even body row, size switches to compact text, contained (default) draws the rounded border, and clickable adds a hover/pointer affordance to a row. Column alignment uses logical start/end.
| Patient | Clinic | Visits |
|---|---|---|
| Amina Farouk | Cardiology | 12 |
| Youssef Kamal | Radiology | 4 |
| Layla Hassan | Oncology | 9 |
Click a row to select it.
<script setup lang="ts">
import { ref } from 'vue'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@myghf/ui'
const patients = [
{ id: 1, name: 'Amina Farouk', clinic: 'Cardiology', visits: 12 },
{ id: 2, name: 'Youssef Kamal', clinic: 'Radiology', visits: 4 },
{ id: 3, name: 'Layla Hassan', clinic: 'Oncology', visits: 9 },
]
const selected = ref<number | null>(null)
</script>
<template>
<div class="space-y-3">
<Table striped>
<TableHeader>
<TableRow>
<TableHead>Patient</TableHead>
<TableHead>Clinic</TableHead>
<TableHead align="end">Visits</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="patient in patients"
:key="patient.id"
clickable
@click="selected = patient.id"
>
<TableCell>{{ patient.name }}</TableCell>
<TableCell class="text-muted">{{ patient.clinic }}</TableCell>
<TableCell align="end">{{ patient.visits }}</TableCell>
</TableRow>
</TableBody>
</Table>
<p class="text-sm text-muted">
{{ selected ? `Selected row #${selected}` : 'Click a row to select it.' }}
</p>
</div>
</template>Pagination
TablePagination is controlled through v-model:page-index and v-model:page-size and only needs the total count — slice your data yourself. pageIndex is 0-based.
| Reference | Clinic |
|---|---|
| MYG-001 | Cardiology |
| MYG-002 | Radiology |
| MYG-003 | Oncology |
| MYG-004 | Cardiology |
| MYG-005 | Radiology |
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TablePagination,
TableRow,
} from '@myghf/ui'
const rows = Array.from({ length: 23 }, (_, index) => ({
id: index + 1,
ref: `MYG-${String(index + 1).padStart(3, '0')}`,
clinic: ['Cardiology', 'Radiology', 'Oncology'][index % 3],
}))
const pageIndex = ref(0)
const pageSize = ref(5)
const visible = computed(() =>
rows.slice(pageIndex.value * pageSize.value, (pageIndex.value + 1) * pageSize.value),
)
</script>
<template>
<Table>
<TableHeader>
<TableRow>
<TableHead>Reference</TableHead>
<TableHead>Clinic</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in visible" :key="row.id">
<TableCell>{{ row.ref }}</TableCell>
<TableCell class="text-muted">{{ row.clinic }}</TableCell>
</TableRow>
</TableBody>
</Table>
<TablePagination
v-model:page-index="pageIndex"
v-model:page-size="pageSize"
:total="rows.length"
:page-size-options="[5, 10, 20]"
/>
</template>Empty state
TableEmpty renders one full-width row that is empty by default; pass columns so its colspan matches the table, and provide a label or the default slot for the message.
| Reference | Clinic |
|---|---|
| MYG-001 | Cardiology |
| MYG-002 | Radiology |
<script setup lang="ts">
import { ref } from 'vue'
import {
Button,
Table,
TableBody,
TableCell,
TableEmpty,
TableHead,
TableHeader,
TableRow,
} from '@myghf/ui'
const showEmpty = ref(false)
const rows = [
{ id: 1, ref: 'MYG-001', clinic: 'Cardiology' },
{ id: 2, ref: 'MYG-002', clinic: 'Radiology' },
]
</script>
<template>
<div class="space-y-3">
<Table>
<TableHeader>
<TableRow>
<TableHead>Reference</TableHead>
<TableHead>Clinic</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<template v-if="showEmpty">
<TableEmpty :columns="2" label="No referrals yet" />
</template>
<TableRow v-for="row in rows" v-else :key="row.id">
<TableCell>{{ row.ref }}</TableCell>
<TableCell class="text-muted">{{ row.clinic }}</TableCell>
</TableRow>
</TableBody>
</Table>
<Button variant="outline" size="sm" @click="showEmpty = !showEmpty">
{{ showEmpty ? 'Show rows' : 'Show empty state' }}
</Button>
</div>
</template>Props
Table
| Prop | Type | Default | Description |
|---|---|---|---|
striped | boolean | false | Shades every even body row with bg-surface-muted/40. |
size | 'sm' | 'default' | 'default' | sm renders text-xs, default renders text-sm. |
contained | boolean | true | Wraps the table in a rounded border with a surface background. |
TableHeader / TableBody
No props. They render <thead> and <tbody> respectively.
TableRow
| Prop | Type | Default | Description |
|---|---|---|---|
clickable | boolean | false | Adds a pointer cursor and a hover background. The click is not wired for you — attach @click. |
TableHead
| Prop | Type | Default | Description |
|---|---|---|---|
sortDirection | 'ascending' | 'descending' | 'none' | — | Sets aria-sort; 'none' omits it. Visual/AT state only — it does not sort. |
width | string | — | Applied as the cell's inline width. |
align | 'start' | 'center' | 'end' | 'start' | Text alignment using logical properties. |
TableCell
| Prop | Type | Default | Description |
|---|---|---|---|
align | 'start' | 'center' | 'end' | 'start' | Text alignment using logical properties. |
truncate | boolean | — (falsy) | Caps the cell at max-w-0 and truncates overflowing text with an ellipsis. |
TableEmpty
| Prop | Type | Default | Description |
|---|---|---|---|
columns | number | — (required) | Number of columns to span, so the row lines up with the header. |
label | string | — | Message rendered when no default slot is provided. |
TablePagination
| Prop | Type | Default | Description |
|---|---|---|---|
pageIndex | number | — (required) | 0-based index of the current page. Bind with v-model:page-index. |
pageSize | number | — (required) | Rows per page. Bind with v-model:page-size. |
total | number | — (required) | Total number of rows across all pages. |
pageSizeOptions | number[] | [5, 10, 20] | Options shown in the rows-per-page select. |
rowsPerPageLabel | string | 'Rows per page' | Label beside the page-size select. |
ofLabel | string | 'of' | Word between the visible range and the total. |
previousLabel | string | 'Previous page' | aria-label of the previous button. |
nextLabel | string | 'Next page' | aria-label of the next button. |
Events
| Component | Event | Payload | Description |
|---|---|---|---|
TablePagination | update:pageIndex | number | Requested page change. |
TablePagination | update:pageSize | number | Requested page-size change. |
Table, TableHeader, TableBody, TableRow, TableHead, TableCell, and TableEmpty declare no custom events. Native listeners fall through to their root element (for example, @click on TableRow).
Slots
| Component | Slot | Description |
|---|---|---|
Table | default | Table content (TableHeader then TableBody). |
TableHeader | default | Header rows. |
TableBody | default | Body rows. |
TableRow | default | Cells. |
TableHead | default | Header cell content. |
TableCell | default | Cell content. |
TableEmpty | default | Overrides label. |
TablePagination renders no slots.
Exposed methods
None. No component in the family calls defineExpose.
Accessibility
Tablerenders a plain<table>. Attributes fall through, so passaria-label(or a<caption>as the first child of the default slot) to name the table.TableHeadrenders<th scope="col">and writesaria-sortonly forascending/descending. It does not make the header interactive: wrap the label in your own<button>when the column is sortable, or let DataTable do it.TableEmptyspans every column withcolspanso the message is announced in context.TablePaginationuses a native<select>and real<button>s. The active page button getsaria-current="page", previous/next buttons carryaria-labels, and both are disabled at the first/last page.
Dark mode & RTL
- The contained wrapper uses
border-border/bg-surface, the headerbg-surface-muted/60, stripingbg-surface-muted/40, and rowsborder-border/60— all semantic tokens, so light and dark both adapt. - Alignment uses
text-start/text-end, and cell padding is symmetric (px-3 py-2), so the table mirrors with no overrides when the container isdir="rtl". TablePaginationlays out with flex andjustify-between; its previous/next chevrons usertl:rotate-180, so they point the right way in bidi layouts.