Skip to content
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

Invoices 236

InvoiceStatusDueAmountActions
INV-1042
Kaveri Foods
Paid12 Oct 2026₹48,500
INV-1041
Nimbus Interiors
Overdue28 Sep 2026₹1,12,000
INV-1040
Kavya Textiles
Due soon20 Oct 2026₹26,750
INV-1039
Ardent Clinics
Draft—₹9,800
INV-1038
Bluefern Café
Paid2 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.