Skip to content

HiTable Data Grid

A flexible, feature-rich data table component built with Vue 3, TypeScript, and TanStack Table. The component provides a clean API with extensive customization options while maintaining excellent performance for enterprise applications.

🔄 Sorting & Filtering

Single and multi-column sorting with global search and column-specific filters

📄 Pagination

Configurable page sizes, navigation controls, and page information display

✅ Row Selection

Single and multi-row selection with select all functionality and bulk actions

👁️ Column Visibility

Dynamic show/hide columns with user preference persistence

📊 Export Integration

Seamless integration with HiExport for CSV exports with multiple options

🎨 Customizable UI

Responsive design, dark mode support, loading states, and custom styling

Basic Table Example
<template>
<HiTable
:data="users"
:columns="columns"
/>
</template>
<script setup lang="ts">
import { HiTable } from '@/components/HiTable'
import type { ColumnDef } from '@tanstack/vue-table'
interface User {
id: string
name: string
email: string
role: string
}
const users = ref<User[]>([
{ id: '1', name: 'John Doe', email: '[email protected]', role: 'Admin' },
{ id: '2', name: 'Jane Smith', email: '[email protected]', role: 'User' },
])
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'role',
header: 'Role',
},
]
</script>

The main configuration object for customizing table behavior:

PropertyTypeDefaultDescription
pagination.enabledbooleantrueEnable/disable pagination
pagination.pageSizenumber10Initial page size
pagination.showPageInfobooleantrueShow “X-Y of Z” info
sorting.enabledbooleantrueEnable/disable sorting
sorting.multiSortbooleanfalseAllow multi-column sorting
filtering.enabledbooleantrueEnable/disable filtering
filtering.searchablebooleantrueShow global search input
rowSelection.enabledbooleanfalseEnable row selection
columnVisibility.enabledbooleantrueEnable column visibility toggle
expandable.enabledbooleanfalseEnable row expansion
<script setup lang="ts">
import { Checkbox } from '@/components/ui/checkbox'
const columns: ColumnDef<User>[] = [
// Selection column
{
id: 'select',
header: ({ table }) =>
h(Checkbox, {
checked: table.getIsAllPageRowsSelected(),
'onUpdate:checked': (value) => table.toggleAllPageRowsSelected(!!value),
}),
cell: ({ row }) =>
h(Checkbox, {
checked: row.getIsSelected(),
'onUpdate:checked': (value) => row.toggleSelected(!!value),
}),
enableSorting: false,
enableHiding: false,
},
// ... other columns
]
</script>

The HiTable component provides several slots for customization:

<template>
<HiTable
:data="orders"
:columns="columns"
:config="config"
>
<!-- Custom toolbar actions -->
<template #toolbar-actions="{table}">
<Button
@click="exportData"
variant="outline"
size="sm"
>
Export CSV
</Button>
<Button
@click="refreshData"
variant="outline"
size="sm"
>
Refresh
</Button>
</template>
<!-- Custom toolbar filters -->
<template #toolbar-filters="{table}">
<Select @update:model-value="filterByStatus">
<SelectTrigger class="w-40">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
</SelectContent>
</Select>
</template>
</HiTable>
</template>

HiTable seamlessly integrates with the HiExport component for comprehensive data export functionality.

<template>
<HiTable
:data="users"
:columns="columns"
:config="tableConfig"
>
<template #toolbar-actions="{table}">
<HiExport
:table="table"
:original-data="users"
:columns="exportColumns"
filename="users-export"
button-text="Export Data"
/>
</template>
</HiTable>
</template>
<script setup lang="ts">
import { HiExport } from '@/components/HiExport'
import type { ExportColumn } from '@/components/HiExport/types'
const exportColumns: ExportColumn[] = [
{ key: 'id', header: 'User ID' },
{ key: 'name', header: 'Full Name' },
{ key: 'email', header: 'Email Address' },
{
key: 'role',
header: 'User Role',
accessor: (user) => user.role.toUpperCase()
},
]
</script>

Export All - Exports all original data regardless of filters or pagination
Export Selected - Exports only selected rows (requires row selection)
Export Current Page - Exports data visible on current page
Export Filtered - Exports all data matching current filters

🏗️ Component Architecture & Development Practices

Section titled “🏗️ Component Architecture & Development Practices”

The HiTable component follows a modular architecture pattern that promotes maintainability and extensibility:

The component is built using a composition-based architecture:

src/components/HiTable/
├── index.vue # Main component entry point
├── composables/
│ └── useTable.ts # Core table logic & state management
├── components/
│ ├── TableContent.vue # Table rendering & virtualization
│ ├── TableToolbar.vue # Search, filters, column visibility
│ └── TablePagination.vue # Pagination controls
└── types/
└── index.ts # TypeScript type definitions

Key Design Principles:

  • Separation of Concerns: Each sub-component handles specific functionality
  • Composable Logic: Business logic isolated in reusable composables
  • Type Safety: Comprehensive TypeScript interfaces for all configurations
  • Slot-based Extensibility: Multiple slots for custom content injection

You can create reusable column definitions for common patterns. Here’s an example approach:

Column Pattern Examples
// Example: Creating reusable column patterns
import { h, type ColumnDef } from '@tanstack/vue-table'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
// Example: Selection column pattern
const selectionColumn: ColumnDef<User> = {
id: 'select',
header: ({ table }) => h(Checkbox, {
checked: table.getIsAllPageRowsSelected(),
'onUpdate:checked': (value) => table.toggleAllPageRowsSelected(!!value),
ariaLabel: 'Select all rows'
}),
cell: ({ row }) => h(Checkbox, {
checked: row.getIsSelected(),
'onUpdate:checked': (value) => row.toggleSelected(!!value),
ariaLabel: `Select row ${row.index + 1}`
}),
enableSorting: false,
enableHiding: false,
}
// Example: Status badge column pattern
const statusColumn: ColumnDef<User> = {
accessorKey: 'status',
header: 'Status',
cell: ({ getValue }) => {
const status = getValue() as string
const variants: Record<string, string> = {
active: 'default',
inactive: 'secondary',
pending: 'outline'
}
return h(Badge, { variant: variants[status] || 'secondary' }, () => status)
},
}
// Example: Actions column pattern
const actionsColumn: ColumnDef<User> = {
id: 'actions',
header: 'Actions',
cell: ({ row }) => {
return h('div', { class: 'flex gap-2' }, [
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => console.log('Edit', row.original)
}, () => 'Edit'),
h(Button, {
variant: 'ghost',
size: 'sm',
onClick: () => console.log('Delete', row.original)
}, () => 'Delete')
])
},
enableSorting: false,
}
Data Validation Pattern
// types/tableData.ts - Data validation schemas
import { z } from 'zod'
// Define data schemas for type safety
const UserSchema = z.object({
id: z.string().min(1),
name: z.string().min(2),
email: z.string().email(),
role: z.enum(['admin', 'user', 'moderator']),
status: z.enum(['active', 'inactive', 'suspended']),
createdAt: z.string().datetime(),
})
type User = z.infer<typeof UserSchema>
// Data validation utility
export function validateTableData<T>(data: unknown[], schema: z.ZodSchema<T>): T[] {
const validatedData: T[] = []
const errors: string[] = []
data.forEach((item, index) => {
try {
const validItem = schema.parse(item)
validatedData.push(validItem)
} catch (error) {
if (error instanceof z.ZodError) {
errors.push(`Row ${index + 1}: ${error.errors.map(e => e.message).join(', ')}`)
}
}
})
if (errors.length > 0) {
console.warn('Data validation errors:', errors)
// Optionally show user notification
toast.warning(`${errors.length} rows have validation errors`)
}
return validatedData
}
// Usage in component
const safeUserData = computed(() =>
validateTableData(rawUserData.value, UserSchema)
)

Access table methods through a template ref for programmatic control:

<template>
<div>
<div class="mb-4 space-x-2">
<Button @click="selectAll">Select All</Button>
<Button @click="clearSelection">Clear Selection</Button>
<Button @click="resetFilters">Reset Filters</Button>
</div>
<HiTable
ref="tableRef"
:data="data"
:columns="columns"
:config="config"
/>
</div>
</template>
<script setup lang="ts">
const tableRef = ref()
const selectAll = () => {
tableRef.value?.table.toggleAllRowsSelected(true)
}
const clearSelection = () => {
tableRef.value?.table.resetRowSelection()
}
const resetFilters = () => {
tableRef.value?.table.resetColumnFilters()
}
// Advanced state management
const tableState = computed(() => {
if (!tableRef.value?.table) return null
const table = tableRef.value.table
return {
selectedCount: table.getSelectedRowModel().rows.length,
totalRows: table.getRowModel().rows.length,
currentPage: table.getState().pagination.pageIndex + 1,
pageSize: table.getState().pagination.pageSize,
totalPages: table.getPageCount(),
hasFilters: table.getState().columnFilters.length > 0,
sorting: table.getState().sorting,
isAllSelected: table.getIsAllPageRowsSelected(),
}
})
// Bulk operations with selected rows
const performBulkAction = async (action: 'delete' | 'export' | 'update', data?: any) => {
const selectedRows = tableRef.value?.table.getSelectedRowModel().rows
if (!selectedRows?.length) {
toast.warning('Please select rows to perform this action')
return
}
const selectedData = selectedRows.map(row => row.original)
try {
switch (action) {
case 'delete':
await bulkDeleteUsers(selectedData.map(user => user.id))
toast.success(`Deleted ${selectedData.length} users`)
break
case 'export':
await exportUsersToCSV(selectedData)
toast.success('Export completed')
break
case 'update':
await bulkUpdateUsers(selectedData.map(user => ({ ...user, ...data })))
toast.success(`Updated ${selectedData.length} users`)
break
}
// Clear selection after action
tableRef.value?.table.resetRowSelection()
// Refresh data
await refetchData()
} catch (error) {
toast.error(`Failed to ${action} selected rows`)
console.error(`Bulk ${action} error:`, error)
}
}
// Advanced filtering and search
const applyAdvancedFilter = (filters: Record<string, any>) => {
const table = tableRef.value?.table
if (!table) return
// Clear existing filters
table.resetColumnFilters()
// Apply new filters
Object.entries(filters).forEach(([columnId, value]) => {
if (value !== undefined && value !== null && value !== '') {
table.getColumn(columnId)?.setFilterValue(value)
}
})
}
// Export current view data
const exportCurrentView = () => {
const table = tableRef.value?.table
if (!table) return
// Get filtered and sorted data
const currentData = table.getFilteredRowModel().rows.map(row => row.original)
// Export using HiExport or custom logic
exportData(currentData, {
filename: `users-filtered-${new Date().toISOString().split('T')[0]}`,
includeHeaders: true
})
}
// Real-time data synchronization
const syncInterval = ref<NodeJS.Timeout | null>(null)
const startRealtimeSync = (intervalMs = 30000) => {
if (syncInterval.value) return
syncInterval.value = setInterval(async () => {
try {
// Fetch latest data without showing loading state
const latestData = await fetchUsers({ silent: true })
// Check if data has changed
if (JSON.stringify(latestData) !== JSON.stringify(users.value)) {
users.value = latestData
toast.info('Data updated automatically')
}
} catch (error) {
console.error('Real-time sync error:', error)
}
}, intervalMs)
}
const stopRealtimeSync = () => {
if (syncInterval.value) {
clearInterval(syncInterval.value)
syncInterval.value = null
}
}
// Cleanup on unmount
onUnmounted(() => {
stopRealtimeSync()
})
</script>
PropTypeDefaultDescription
dataT[]-Array of data objects to display
columnsColumnDef<T>[]-Column definitions
configTableConfigTable configuration options
toolbarConfigToolbarConfig-Toolbar-specific configuration
paginationConfigPaginationConfig-Pagination-specific configuration
loadingbooleanfalseShow loading state
classstring-Additional CSS classes
SlotPropsDescription
toolbar-actions{table}Custom actions in the toolbar
toolbar-filters{table}Custom filters in the toolbar
expanded-row{row, data}Custom content for expanded rows
empty-state-Custom empty state content
MethodDescription
table.getSelectedRowModel()Get selected rows
table.resetRowSelection()Clear row selection
table.resetColumnFilters()Clear all filters
table.resetSorting()Clear sorting
table.setPageSize(size)Set page size
table.setPageIndex(index)Go to specific page

The component uses Tailwind CSS classes and supports extensive customization:

const tableConfig: TableConfig = {
styling: {
headerClass: 'bg-gray-100 dark:bg-gray-800 font-semibold',
cellClass: 'text-sm',
rowClass: 'hover:bg-gray-50 dark:hover:bg-gray-800',
tableClass: 'border-collapse',
containerClass: 'rounded-lg border',
compact: false, // Use compact padding
}
}
/* Override theme colors */
.hi-table {
--table-header-bg: theme('colors.gray.100');
--table-row-hover: theme('colors.gray.50');
--table-border: theme('colors.gray.200');
}
.dark .hi-table {
--table-header-bg: theme('colors.gray.800');
--table-row-hover: theme('colors.gray.800');
--table-border: theme('colors.gray.700');
}
<script setup lang="ts">
// ✅ Good: Memoize columns
const columns = shallowRef<ColumnDef<User>[]>([
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'email', header: 'Email' },
])
// ✅ Good: Debounce search
const debouncedSearch = useDebounceFn((value: string) => {
// Apply search filter
}, 300)
// ❌ Avoid: Creating columns in template
// const columns = computed(() => [...])
</script>
<template>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold">User Management</h1>
<p class="text-sm text-muted-foreground">
Manage and export user data
</p>
</div>
</div>
<BaseCard>
<div class="p-6">
<HiTable
:data="users"
:columns="columns"
:config="tableConfig"
:loading="loading"
>
<template #toolbar-actions="{table}">
<HiExport
:table="table"
:original-data="users"
:columns="exportColumns"
filename="users-export"
button-text="Export"
/>
<Button @click="refreshData" variant="outline" size="sm">
<RefreshCw class="h-4 w-4 mr-2" />
Refresh
</Button>
</template>
<template #expanded-row="{ data }">
<div class="p-4 bg-gray-50 dark:bg-gray-900 rounded-lg">
<div class="grid grid-cols-2 gap-4">
<div>
<p><strong>Phone:</strong> {{ data.phone }}</p>
<p><strong>Department:</strong> {{ data.department }}</p>
</div>
<div>
<p><strong>Last Login:</strong> {{ formatDate(data.lastLogin) }}</p>
<p><strong>Created:</strong> {{ formatDate(data.createdAt) }}</p>
</div>
</div>
</div>
</template>
</HiTable>
</div>
</BaseCard>
</div>
</template>