Skip to main content

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>

2. Header Pattern

<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>

Action Menu Pattern

For table actions, use DropdownMenu with AlertDialog for destructive actions.

Rules

RuleDoDon’t
Action iconMoreVerticalMoreHorizontal
Actions headerEmpty <TableHead>”Actions” text
Column widthw-[50px]w-[100px] or wider
Dropdown alignmentalign="end"align="start"
Destructive actionstext-destructive classDefault 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>

Adding Pagination

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

  1. Replace HTML elements with shadcn-vue components:
    • <table><Table>
    • <thead><TableHeader>
    • <tbody><TableBody>
    • <tr><TableRow>
    • <th><TableHead>
    • <td><TableCell>
  2. Update imports:
    import {
      Table,
      TableBody,
      TableCell,
      TableHead,
      TableHeader,
      TableRow,
    } from '@/components/ui/table'
    
  3. Add proper empty state handling
  4. Update action menus to use AlertDialog for destructive actions
  5. 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'
}