All Taste components
Data display
Data table
A list of records with a toolbar, row selection, status, right-aligned amounts and row actions.
toolbar · search · selection · status badges · numeric columns · row actions · footer
| Invoice | Status | Due | Amount | Actions | |
|---|---|---|---|---|---|
INV-1042 Kaveri Foods | Paid | 12 Oct 2026 | ₹48,500 | ||
INV-1041 Nimbus Interiors | Overdue | 28 Sep 2026 | ₹1,12,000 | ||
INV-1040 Kavya Textiles | Due soon | 20 Oct 2026 | ₹26,750 | ||
INV-1039 Ardent Clinics | Draft | — | ₹9,800 | ||
INV-1038 Bluefern Café | Paid | 2 Oct 2026 | ₹14,200 |
1 of 236 selectedPage 1 of 48
"use client";
import { useState } from "react";
import { Filter, MoreHorizontal, Plus, Search } from "lucide-react";
import { cn } from "@/lib/utils";
type Status = "paid" | "overdue" | "due" | "draft";
type Invoice = { id: string; customer: string; status: Status; due?: string; amount: number };
const statusStyle: Record<Status, { label: string; className: string }> = {
paid: { label: "Paid", className: "bg-success-soft text-success-fg" },
overdue: { label: "Overdue", className: "bg-danger-soft text-danger-fg" },
due: { label: "Due soon", className: "bg-warning-soft text-warning-fg" },
draft: { label: "Draft", className: "bg-surface-2 text-fg-muted" },
};
const inr = new Intl.NumberFormat("en-IN", { style: "currency", currency: "INR", maximumFractionDigits: 0 });
export function InvoiceTable({ invoices }: { invoices: Invoice[] }) {
const [selected, setSelected] = useState<Set<string>>(new Set());
const [query, setQuery] = useState("");
const rows = invoices.filter((i) => (i.id + " " + i.customer).toLowerCase().includes(query.toLowerCase()));
const allChecked = rows.length > 0 && rows.every((r) => selected.has(r.id));
const toggle = (id: string) =>
setSelected((s) => { const n = new Set(s); if (n.has(id)) n.delete(id); else n.add(id); return n; });
return (
<div className="overflow-hidden rounded-lg border border-border bg-bg">
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 border-b border-border py-3 pl-4 pr-3">
<h3 className="mr-auto flex items-center gap-2 text-[14.5px] font-semibold">
Invoices <span className="rounded-full bg-surface-2 px-2 text-xs font-medium text-fg-muted">{invoices.length}</span>
</h3>
<label className="relative min-w-[140px] flex-[0_1_200px]">
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-fg-subtle" aria-hidden />
<input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search invoices" aria-label="Search invoices"
className="h-[30px] w-full rounded border border-border-strong bg-bg pl-[30px] pr-3 text-[13px] text-fg outline-none placeholder:text-fg-subtle focus:border-primary focus:ring-[3px] focus:ring-primary/20" />
</label>
<button type="button" className="inline-flex h-[30px] items-center gap-1.5 rounded border border-border-strong px-2.5 text-[13px] font-medium hover:bg-surface">
<Filter className="size-4" aria-hidden />Filter
</button>
<button type="button" className="inline-flex h-[30px] items-center gap-1.5 rounded bg-primary px-2.5 text-[13px] font-medium text-primary-fg hover:bg-primary-hover">
<Plus className="size-4" aria-hidden />New invoice
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full min-w-[600px] border-collapse text-[13.5px]">
<thead className="bg-surface text-left text-xs text-fg-subtle">
<tr className="border-b border-border">
<th scope="col" className="w-11 py-2 pl-4">
<input type="checkbox" aria-label="Select all" checked={allChecked} className="size-4 accent-primary"
onChange={() => setSelected(allChecked ? new Set() : new Set(rows.map((r) => r.id)))} />
</th>
<th scope="col" className="px-3 py-2 font-medium">Invoice</th>
<th scope="col" className="px-3 py-2 font-medium">Status</th>
<th scope="col" className="px-3 py-2 font-medium">Due</th>
<th scope="col" className="px-3 py-2 text-right font-medium">Amount</th>
<th scope="col" className="w-12"><span className="sr-only">Actions</span></th>
</tr>
</thead>
<tbody>
{rows.map((inv) => {
const s = statusStyle[inv.status];
const isSel = selected.has(inv.id);
return (
<tr key={inv.id} aria-selected={isSel}
className={cn("border-b border-border last:border-0 hover:bg-surface", isSel && "bg-primary-soft/75 hover:bg-primary-soft")}>
<td className="py-2.5 pl-4">
<input type="checkbox" checked={isSel} onChange={() => toggle(inv.id)} aria-label={"Select " + inv.id} className="size-4 accent-primary" />
</td>
<td className="px-3 py-2.5">
<div className="font-medium tabular-nums">{inv.id}</div>
<div className="text-[12.5px] text-fg-subtle">{inv.customer}</div>
</td>
<td className="px-3 py-2.5">
<span className={cn("inline-flex h-[22px] items-center rounded-full px-2 text-xs font-medium", s.className)}>{s.label}</span>
</td>
<td className="whitespace-nowrap px-3 py-2.5 text-fg-muted">{inv.due ?? "—"}</td>
<td className="px-3 py-2.5 text-right font-medium tabular-nums">{inr.format(inv.amount)}</td>
<td className="pr-3 text-right">
<button type="button" aria-label={"Actions for " + inv.id}
className="inline-grid size-7 place-items-center rounded text-fg-subtle hover:bg-surface-2 hover:text-fg">
<MoreHorizontal className="size-4" aria-hidden />
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-border py-2.5 pl-4 pr-3 text-[13px] text-fg-muted">
<span><b className="font-medium text-fg">{selected.size}</b> of {invoices.length} selected</span>
</div>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.