Data Table
The sortable, filterable data table is the primary way dense data is displayed across all 8 web portals. It follows consistent column type conventions and header styling.
Column Types
| Type | Font | Alignment | Example |
|---|---|---|---|
| Text | font-sans regular | Left | Product name, store name |
| Monospace | font-mono 13px | Left | Transaction IDs, dates, SKU codes |
| Amount | font-mono 13px, weight 600 | Right | Rp 1.450.000, quantities |
| Badge | Pill, 10px uppercase | Left | Status badges (see below) |
| Avatar | 32px circle + name text | Left | Cashier name, employee |
| Action | Icon buttons (ghost) | Right | Edit, delete, view detail |
Badge Column Variants
| Status | Background | Text | Dot |
|---|---|---|---|
| Active / Selesai | #E8F0EC | #2D6B4A | #2D7D46 |
| Pending / Menunggu | #FEF3C7 | #92400E | #D4910A |
| Failed / Gagal | #FCEAE8 | #9B1C1C | #C4463A |
| Draft | #F3F1EC | #57534E | #A8A29E |
Dense tables
For dense tables (>10 rows visible), replace full badge pills with tiny 6px status dots to save horizontal space. The dot color alone is sufficient when combined with a column header like "Status".
Header Styling
- Uppercase, 11px,
text-2,font-sans, letter-spacing0.05em - Background:
bg-inset(#F3F1EC) - Height: 40px, vertically centered
- Sort indicator: Lucide
ChevronUp/ChevronDownat 14px,text-3color - Active sort: icon becomes
text-1, column header becomestext-1
Row Styling
| State | Background | Border |
|---|---|---|
| Default | bg-card | Bottom border 1px |
| Hover | bg-hover | Same |
| Selected | green-light (#E8F0EC) | Same |
| Striped (optional) | Alternate bg-inset | Same |
React + TanStack Table Example
tsx
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
flexRender,
type ColumnDef,
} from '@tanstack/react-table'
interface Transaction {
id: string
date: string
customer: string
amount: number
status: 'completed' | 'pending' | 'failed'
}
const columns: ColumnDef<Transaction>[] = [
{
accessorKey: 'id',
header: 'ID Transaksi',
cell: ({ getValue }) => (
<span className="font-mono text-[13px]">{getValue<string>()}</span>
),
},
{
accessorKey: 'date',
header: 'Tanggal',
cell: ({ getValue }) => (
<span className="font-mono text-[13px]">{getValue<string>()}</span>
),
},
{
accessorKey: 'customer',
header: 'Pelanggan',
},
{
accessorKey: 'amount',
header: () => <span className="block text-right">Jumlah</span>,
cell: ({ getValue }) => (
<span className="block text-right font-mono text-[13px] font-semibold">
Rp {getValue<number>().toLocaleString('id-ID')}
</span>
),
},
{
accessorKey: 'status',
header: 'Status',
cell: ({ getValue }) => {
const status = getValue<string>()
const styles = {
completed: 'bg-green-light text-green-700',
pending: 'bg-amber-100 text-amber-800',
failed: 'bg-red-light text-red-800',
}
return (
<span className={cn('px-2 py-0.5 rounded-full text-[10px] uppercase font-semibold', styles[status])}>
{status}
</span>
)
},
},
]
export function TransactionTable({ data }: { data: Transaction[] }) {
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
})
return (
<div className="border border-border rounded-xl overflow-hidden">
<table className="w-full">
<thead>
{table.getHeaderGroups().map(hg => (
<tr key={hg.id} className="bg-inset">
{hg.headers.map(h => (
<th
key={h.id}
onClick={h.column.getToggleSortingHandler()}
className="px-4 h-10 text-[11px] uppercase tracking-wider text-2 font-semibold text-left cursor-pointer select-none"
>
{flexRender(h.column.columnDef.header, h.getContext())}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map(row => (
<tr key={row.id} className="border-b border-border hover:bg-hover transition-colors">
{row.getVisibleCells().map(cell => (
<td key={cell.id} className="px-4 py-3 text-sm">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}Filter Bar
Place a filter bar above the table inside the same card container:
- Search input: left-aligned,
min-width: 240px, LucideSearchicon - Filter dropdowns: status, date range, store (varies by context)
- Active filter count badge on the filter icon
Pagination
- Bottom of table, inside the card
- "Menampilkan 1--20 dari 247" text on the left
- Page buttons on the right:
<123...13> - Active page:
bg-brandwhite text, others: ghost style
Don't
- Don't add row borders on the left/right sides --- only bottom borders
- Don't use colored row backgrounds for status --- use badge columns instead
- Don't center-align text columns --- only amounts are right-aligned