Back to components
Data Display

Data Table

A compact admin table with selection, row actions, badges, avatars, and pagination.

Page preview
Source
This is the component source that shadcn installs into a consuming project.
registry/default/data-table/data-table.tsx
import { MoreHorizontalIcon, SlidersHorizontalIcon } from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
  Pagination,
  PaginationContent,
  PaginationItem,
  PaginationLink,
  PaginationNext,
  PaginationPrevious,
} from "@/components/ui/pagination"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"

type DataTableRow = {
  id: string
  name: string
  email: string
  initials: string
  role: string
  status: string
  plan: string
  activity: string
}

type DataTableCopy = {
  title: string
  description: string
  filterLabel: string
  selectedLabel: string
  moreLabel: string
  menuLabel: string
  menuItems: [string, string, string]
  columns: {
    member: string
    role: string
    status: string
    plan: string
    activity: string
  }
  previous: string
  next: string
  rows: DataTableRow[]
}

type DataTableProps = {
  copy?: DataTableCopy
}

const defaultCopy: DataTableCopy = {
  title: "Members",
  description: "A compact table for admin lists, ownership reviews, and account pages.",
  filterLabel: "Filters",
  selectedLabel: "3 selected",
  moreLabel: "Open row actions",
  menuLabel: "Row actions",
  menuItems: ["Open profile", "Copy email", "Remove access"],
  columns: {
    member: "Member",
    role: "Role",
    status: "Status",
    plan: "Plan",
    activity: "Last active",
  },
  previous: "Previous",
  next: "Next",
  rows: [
    {
      id: "1",
      name: "Avery Brooks",
      email: "avery@example.com",
      initials: "AB",
      role: "Owner",
      status: "Active",
      plan: "Pro",
      activity: "2m ago",
    },
    {
      id: "2",
      name: "Mina Park",
      email: "mina@example.com",
      initials: "MP",
      role: "Designer",
      status: "Invited",
      plan: "Team",
      activity: "1h ago",
    },
    {
      id: "3",
      name: "Noah Chen",
      email: "noah@example.com",
      initials: "NC",
      role: "Engineer",
      status: "Active",
      plan: "Pro",
      activity: "Yesterday",
    },
  ],
}

export function DataTable({ copy = defaultCopy }: DataTableProps) {
  return (
    <Card className="w-full max-w-4xl">
      <CardHeader>
        <div className="flex flex-wrap items-start justify-between gap-3">
          <div className="flex min-w-0 flex-col gap-1">
            <CardTitle>{copy.title}</CardTitle>
            <CardDescription>{copy.description}</CardDescription>
          </div>
          <div className="flex flex-wrap gap-2">
            <Badge variant="secondary">{copy.selectedLabel}</Badge>
            <Button variant="outline" size="sm">
              <SlidersHorizontalIcon data-icon="inline-start" />
              {copy.filterLabel}
            </Button>
          </div>
        </div>
      </CardHeader>
      <CardContent>
        <Table>
          <TableHeader>
            <TableRow>
              <TableHead className="w-10">
                <Checkbox aria-label="Select all rows" />
              </TableHead>
              <TableHead>{copy.columns.member}</TableHead>
              <TableHead>{copy.columns.role}</TableHead>
              <TableHead>{copy.columns.status}</TableHead>
              <TableHead>{copy.columns.plan}</TableHead>
              <TableHead>{copy.columns.activity}</TableHead>
              <TableHead className="w-10">
                <span className="sr-only">{copy.moreLabel}</span>
              </TableHead>
            </TableRow>
          </TableHeader>
          <TableBody>
            {copy.rows.map((row) => (
              <TableRow key={row.id}>
                <TableCell>
                  <Checkbox aria-label={`Select ${row.name}`} />
                </TableCell>
                <TableCell>
                  <div className="flex min-w-48 items-center gap-3">
                    <Avatar size="sm">
                      <AvatarFallback>{row.initials}</AvatarFallback>
                    </Avatar>
                    <div className="flex min-w-0 flex-col">
                      <span className="truncate font-medium">{row.name}</span>
                      <span className="truncate text-xs text-muted-foreground">
                        {row.email}
                      </span>
                    </div>
                  </div>
                </TableCell>
                <TableCell>{row.role}</TableCell>
                <TableCell>
                  <Badge
                    variant={
                      row.status === "Active" || row.status === "已启用"
                        ? "default"
                        : "outline"
                    }
                  >
                    {row.status}
                  </Badge>
                </TableCell>
                <TableCell>{row.plan}</TableCell>
                <TableCell className="text-muted-foreground">
                  {row.activity}
                </TableCell>
                <TableCell>
                  <DropdownMenu>
                    <DropdownMenuTrigger
                      render={
                        <Button
                          aria-label={copy.moreLabel}
                          size="icon-sm"
                          variant="ghost"
                        />
                      }
                    >
                      <MoreHorizontalIcon />
                    </DropdownMenuTrigger>
                    <DropdownMenuContent align="end" className="w-40">
                      <DropdownMenuLabel>{copy.menuLabel}</DropdownMenuLabel>
                      <DropdownMenuGroup>
                        <DropdownMenuItem>{copy.menuItems[0]}</DropdownMenuItem>
                        <DropdownMenuItem>{copy.menuItems[1]}</DropdownMenuItem>
                      </DropdownMenuGroup>
                      <DropdownMenuSeparator />
                      <DropdownMenuGroup>
                        <DropdownMenuItem variant="destructive">
                          {copy.menuItems[2]}
                        </DropdownMenuItem>
                      </DropdownMenuGroup>
                    </DropdownMenuContent>
                  </DropdownMenu>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </CardContent>
      <CardFooter className="justify-between gap-3">
        <Pagination className="mx-0 justify-start">
          <PaginationContent>
            <PaginationItem>
              <PaginationPrevious href="#" text={copy.previous} />
            </PaginationItem>
            <PaginationItem>
              <PaginationLink href="#" isActive>
                1
              </PaginationLink>
            </PaginationItem>
            <PaginationItem>
              <PaginationLink href="#">2</PaginationLink>
            </PaginationItem>
            <PaginationItem>
              <PaginationNext href="#" text={copy.next} />
            </PaginationItem>
          </PaginationContent>
        </Pagination>
      </CardFooter>
    </Card>
  )
}