Table Design System
This guide shows developers how to implement consistent, accessible data tables in the DeployStack frontend.
Quick Implementation
Basic Table Structure
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { MoreHorizontal, Edit, Trash2 } from 'lucide-vue-next'
const { t } = useI18n()
interface Props {
items: Array<{
id: string
name: string
description?: string
status: string
created_at: string
}>
onEditItem: (item: any) => void
onDeleteItem: (itemId: string) => void
}
const props = defineProps<Props>()
// Sort items by name
const sortedItems = computed(() => {
return [...props.items].sort((a, b) => a.name.localeCompare(b.name))
})
// Format date for display
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString()
}
</script>
<template>
<div class="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>{{ t('table.columns.name') }}</TableHead>
<TableHead>{{ t('table.columns.description') }}</TableHead>
<TableHead>{{ t('table.columns.status') }}</TableHead>
<TableHead>{{ t('table.columns.created') }}</TableHead>
<TableHead class="w-[100px]">{{ t('table.columns.actions') }}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<!-- Empty State -->
<TableRow v-if="sortedItems.length === 0">
<TableCell :colspan="5" class="h-24 text-center">
{{ t('table.noData') }}
</TableCell>
</TableRow>
<!-- Data Rows -->
<TableRow v-for="item in sortedItems" :key="item.id">
<TableCell class="font-medium">{{ item.name }}</TableCell>
<TableCell>
<span v-if="item.description" class="text-sm text-muted-foreground">
{{ item.description }}
</span>
<span v-else class="text-sm text-muted-foreground italic">
{{ t('table.noDescription') }}
</span>
</TableCell>
<TableCell>
<Badge variant="outline">{{ item.status }}</Badge>
</TableCell>
<TableCell class="text-sm text-muted-foreground">
{{ formatDate(item.created_at) }}
</TableCell>
<TableCell>
<!-- Action buttons here -->
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
</template>
shadcn-vue Table Components
Required Components
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
Component Structure
Table
- Main table wrapperTableHeader
- Table header sectionTableBody
- Table body sectionTableRow
- Table row (for both header and body)TableHead
- Header cellTableCell
- Data cell
Design Patterns
1. Container Structure
<div class="rounded-md border">
<Table>
<!-- Table content -->
</Table>
</div>
2. Header Pattern
<TableHeader>
<TableRow>
<TableHead>Column Name</TableHead>
<TableHead>Another Column</TableHead>
<TableHead class="w-[100px]">Actions</TableHead> <!-- Fixed width for actions -->
</TableRow>
</TableHeader>
3. Empty State Handling
<TableBody>
<TableRow v-if="items.length === 0">
<TableCell :colspan="totalColumns" class="h-24 text-center">
{{ t('table.noData') }}
</TableCell>
</TableRow>
<!-- Regular rows -->
</TableBody>
4. Data Cell Patterns
Primary Content (Names, Titles):
<TableCell class="font-medium">
{{ item.name }}
</TableCell>
Secondary Content (Descriptions, Metadata):
<TableCell>
<span v-if="item.description" class="text-sm text-muted-foreground">
{{ item.description }}
</span>
<span v-else class="text-sm text-muted-foreground italic">
{{ t('table.noDescription') }}
</span>
</TableCell>
Status Indicators:
<TableCell>
<Badge variant="outline">{{ item.status }}</Badge>
</TableCell>
Dates and Timestamps:
<TableCell class="text-sm text-muted-foreground">
{{ formatDate(item.created_at) }}
</TableCell>
Action Menu Pattern
For table actions, use DropdownMenu with AlertDialog for destructive actions:
<script setup lang="ts">
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog'
</script>
<template>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost" class="h-8 w-8 p-0">
<span class="sr-only">{{ t('table.openMenu') }}</span>
<MoreHorizontal class="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem @click="handleEdit(item)">
<Edit class="mr-2 h-4 w-4" />
{{ t('table.actions.edit') }}
</DropdownMenuItem>
<!-- Destructive actions with confirmation -->
<AlertDialog>
<AlertDialogTrigger as-child>
<DropdownMenuItem @click.prevent class="text-destructive focus:text-destructive">
<Trash2 class="mr-2 h-4 w-4" />
{{ t('table.actions.delete') }}
</DropdownMenuItem>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{{ t('deleteDialog.title') }}</AlertDialogTitle>
<AlertDialogDescription>
{{ t('deleteDialog.description', { itemName: item.name }) }}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{{ t('deleteDialog.cancel') }}</AlertDialogCancel>
<AlertDialogAction
@click="handleDelete(item.id)"
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{{ t('deleteDialog.confirm') }}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</template>
Badge Patterns for Tables
Status Badges
<Badge variant="default">Active</Badge>
<Badge variant="secondary">Inactive</Badge>
<Badge variant="destructive">Error</Badge>
<Badge variant="outline">Pending</Badge>
Category/Tag Badges
<Badge variant="secondary" class="font-mono text-xs">
{{ category.icon }}
</Badge>
Numeric Badges
<Badge variant="outline">
{{ item.sort_order }}
</Badge>
Migration from Raw HTML
❌ Deprecated Pattern - Raw HTML Tables
<!-- DON'T DO THIS -->
<table class="w-full">
<thead>
<tr class="border-b">
<th class="h-12 px-4 text-left align-middle font-medium">Name</th>
</tr>
</thead>
<tbody>
<tr class="border-b transition-colors hover:bg-muted/50">
<td class="p-4 align-middle">{{ item.name }}</td>
</tr>
</tbody>
</table>
✅ Preferred Pattern - shadcn-vue Components
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell class="font-medium">{{ item.name }}</TableCell>
</TableRow>
</TableBody>
</Table>
Migration Steps
-
Replace HTML elements with shadcn-vue components:
<table>
→<Table>
<thead>
→<TableHeader>
<tbody>
→<TableBody>
<tr>
→<TableRow>
<th>
→<TableHead>
<td>
→<TableCell>
-
Update imports:
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'
-
Add proper empty state handling
-
Update action menus to use AlertDialog for destructive actions
-
Ensure proper badge usage for status indicators
Required Translations
Add to your i18n file:
table: {
noData: 'No data available',
noDescription: 'No description provided',
openMenu: 'Open menu',
columns: {
name: 'Name',
description: 'Description',
status: 'Status',
created: 'Created',
actions: 'Actions'
},
actions: {
edit: 'Edit',
delete: 'Delete',
view: 'View Details'
}
},
deleteDialog: {
title: 'Delete Item',
description: 'Are you sure you want to delete "{itemName}"? This action cannot be undone.',
cancel: 'Cancel',
confirm: 'Delete'
}