Skip to content
UI

Search docs

Search components and documentation

New
← Back to application blocks

products-01

A products data table with live search, status badges, per-row action menus and responsive columns. Use for catalog, inventory or CRUD list pages.

Code

"use client"

import * as React from "react"
import { MoreHorizontalIcon, PlusIcon, SearchIcon } from "lucide-react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table"

type Product = {
  name: string
  status: "active" | "draft" | "archived"
  price: string
  sales: number
  createdAt: string
}

const products: Product[] = [
  { name: "Laser Lemonade Machine", status: "draft", price: "$499.99", sales: 25, createdAt: "2024-07-12" },
  { name: "Hypernova Headphones", status: "active", price: "$129.99", sales: 100, createdAt: "2024-10-18" },
  { name: "AeroGlow Desk Lamp", status: "active", price: "$39.99", sales: 50, createdAt: "2024-11-29" },
  { name: "TechTonic Energy Drink", status: "draft", price: "$2.99", sales: 0, createdAt: "2024-12-25" },
  { name: "Gamer Gear Pro Controller", status: "active", price: "$59.99", sales: 75, createdAt: "2025-01-01" },
  { name: "Luminous VR Headset", status: "archived", price: "$199.99", sales: 30, createdAt: "2025-02-08" },
]

const statusVariant = {
  active: "default",
  draft: "secondary",
  archived: "outline",
} as const

export function ProductsTable({
  className,
  ...props
}: React.ComponentProps<"div">) {
  const [query, setQuery] = React.useState("")

  const filtered = products.filter((product) =>
    product.name.toLowerCase().includes(query.toLowerCase())
  )

  return (
    <div
      className={cn("mx-auto w-full max-w-4xl px-6 py-16", className)}
      {...props}
    >
      <Card>
        <CardHeader className="flex flex-col gap-4 sm:flex-row sm:items-center">
          <div className="grid flex-1 gap-1.5">
            <CardTitle>Products</CardTitle>
            <CardDescription>
              Manage your products and view their sales performance.
            </CardDescription>
          </div>
          <div className="flex items-center gap-2">
            <div className="relative">
              <SearchIcon className="absolute left-2.5 top-2.5 size-4 text-muted-foreground" />
              <Input
                value={query}
                onChange={(event) => setQuery(event.target.value)}
                placeholder="Search products..."
                className="w-full pl-8 sm:w-[220px]"
                aria-label="Search products"
              />
            </div>
            <Button>
              <PlusIcon className="size-4" />
              Add product
            </Button>
          </div>
        </CardHeader>
        <CardContent>
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>Name</TableHead>
                <TableHead>Status</TableHead>
                <TableHead className="hidden sm:table-cell">Price</TableHead>
                <TableHead className="hidden sm:table-cell">Sales</TableHead>
                <TableHead className="hidden md:table-cell">Created</TableHead>
                <TableHead>
                  <span className="sr-only">Actions</span>
                </TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {filtered.map((product) => (
                <TableRow key={product.name}>
                  <TableCell className="font-medium">{product.name}</TableCell>
                  <TableCell>
                    <Badge variant={statusVariant[product.status]}>
                      {product.status}
                    </Badge>
                  </TableCell>
                  <TableCell className="hidden sm:table-cell">
                    {product.price}
                  </TableCell>
                  <TableCell className="hidden sm:table-cell">
                    {product.sales}
                  </TableCell>
                  <TableCell className="hidden md:table-cell">
                    {product.createdAt}
                  </TableCell>
                  <TableCell>
                    <DropdownMenu>
                      <DropdownMenuTrigger asChild>
                        <Button
                          variant="ghost"
                          size="icon"
                          aria-label={`Actions for ${product.name}`}
                        >
                          <MoreHorizontalIcon className="size-4" />
                        </Button>
                      </DropdownMenuTrigger>
                      <DropdownMenuContent align="end">
                        <DropdownMenuLabel>Actions</DropdownMenuLabel>
                        <DropdownMenuItem>Edit</DropdownMenuItem>
                        <DropdownMenuItem>Duplicate</DropdownMenuItem>
                        <DropdownMenuSeparator />
                        <DropdownMenuItem variant="destructive">
                          Delete
                        </DropdownMenuItem>
                      </DropdownMenuContent>
                    </DropdownMenu>
                  </TableCell>
                </TableRow>
              ))}
              {filtered.length === 0 && (
                <TableRow>
                  <TableCell
                    colSpan={6}
                    className="h-24 text-center text-muted-foreground"
                  >
                    No products found.
                  </TableCell>
                </TableRow>
              )}
            </TableBody>
          </Table>
        </CardContent>
        <CardFooter className="text-sm text-muted-foreground">
          Showing {filtered.length} of {products.length} products
        </CardFooter>
      </Card>
    </div>
  )
}