Documentation Index
Fetch the complete documentation index at: https://docs.deploystack.io/llms.txt
Use this file to discover all available pages before exploring further.
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 { MoreVertical, 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-[50px]"></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 wrapper
TableHeader - Table header section
TableBody - Table body section
TableRow - Table row (for both header and body)
TableHead - Header cell
TableCell - Data cell
Design Patterns
1. Container Structure
<div class="rounded-md border">
<Table>
<!-- Table content -->
</Table>
</div>
<TableHeader>
<TableRow>
<TableHead>Column Name</TableHead>
<TableHead>Another Column</TableHead>
<TableHead class="w-[50px]"></TableHead> <!-- Empty header for actions column -->
</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>
For table actions, use DropdownMenu with AlertDialog for destructive actions.
Rules
| Rule | Do | Don’t |
|---|
| Action icon | MoreVertical | MoreHorizontal |
| Actions header | Empty <TableHead> | ”Actions” text |
| Column width | w-[50px] | w-[100px] or wider |
| Dropdown alignment | align="end" | align="start" |
| Destructive actions | text-destructive class | Default styling |
Implementation
<script setup lang="ts">
import { MoreVertical, Edit, Trash2 } from 'lucide-vue-next'
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>
<MoreVertical 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>
Tables with large datasets should use the PaginationControls component. Place it below the table container.
<template>
<div class="space-y-4">
<div class="rounded-md border">
<Table>
<!-- Table content -->
</Table>
</div>
<PaginationControls
v-if="totalItems > 0"
:current-page="currentPage"
:page-size="pageSize"
:total-items="totalItems"
:is-loading="isLoading"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</template>
For full implementation details including service layer setup, row selection support, and responsive behavior, see the Pagination Guide.
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'
// Note: No 'actions' column - header is empty
},
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'
}