Skip to content

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.

ID
Waktu
Pelanggan
Pembayaran
Status
Total
TXN-28491
14:32
Rina Sari
QRIS
Selesai
287.500
TXN-28490
14:18
Budi Santoso
Tunai
Selesai
154.000
TXN-28489
13:55
Dewi Lestari
Debit
Menunggu
432.000
TXN-28488
13:41
Anto Wijaya
Tunai
Gagal
89.500

Column Types

TypeFontAlignmentExample
Textfont-sans regularLeftProduct name, store name
Monospacefont-mono 13pxLeftTransaction IDs, dates, SKU codes
Amountfont-mono 13px, weight 600RightRp 1.450.000, quantities
BadgePill, 10px uppercaseLeftStatus badges (see below)
Avatar32px circle + name textLeftCashier name, employee
ActionIcon buttons (ghost)RightEdit, delete, view detail

Badge Column Variants

StatusBackgroundTextDot
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-spacing 0.05em
  • Background: bg-inset (#F3F1EC)
  • Height: 40px, vertically centered
  • Sort indicator: Lucide ChevronUp / ChevronDown at 14px, text-3 color
  • Active sort: icon becomes text-1, column header becomes text-1

Row Styling

StateBackgroundBorder
Defaultbg-cardBottom border 1px
Hoverbg-hoverSame
Selectedgreen-light (#E8F0EC)Same
Striped (optional)Alternate bg-insetSame

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, Lucide Search icon
  • 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: < 1 2 3 ... 13 >
  • Active page: bg-brand white 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

RetailOS - Sistem ERP Retail Modern untuk Indonesia