DensityToggle
Compact / Default / Comfortable row density as a small segmented radio group with line-spacing icons; its value drives DensityProvider.
import { DensityToggle } from "@cynco-labs/ui"; Example
import { useState } from "react";
import { DataList, DensityProvider, DensityToggle, formatDate, sumAmounts } from "@cynco-labs/ui";
const L = [
[
"2026-10-01",
"JV-2026-0301",
"1000-001",
"Maybank Current Account",
"Opening balance b/f",
"48250.00",
"",
],
[
"2026-10-02",
"INV-2026-0131",
"1200-001",
"Trade Debtors",
"Syarikat Maju Jaya Sdn Bhd",
"12850.00",
"",
],
[
"2026-10-02",
"INV-2026-0131",
"4000-001",
"Sales — Services",
"Syarikat Maju Jaya Sdn Bhd",
"",
"11898.15",
],
["2026-10-02", "INV-2026-0131", "2300-001", "SST Payable (8%)", "Output tax", "", "951.85"],
[
"2026-10-05",
"BILL-2026-0088",
"6100-002",
"Rent Expense",
"Pelita Hartanah — October rent",
"4500.00",
"",
],
[
"2026-10-05",
"BILL-2026-0088",
"2000-001",
"Trade Creditors",
"Pelita Hartanah Sdn Bhd",
"",
"4500.00",
],
[
"2026-10-09",
"RCPT-2026-0412",
"1000-001",
"Maybank Current Account",
"Receipt — Tropika Kopi",
"3240.00",
"",
],
[
"2026-10-09",
"RCPT-2026-0412",
"1200-001",
"Trade Debtors",
"Tropika Kopi Enterprise",
"",
"3240.00",
],
[
"2026-10-15",
"PAY-2026-0210",
"6200-001",
"Salaries & Wages",
"October payroll",
"18600.00",
"",
],
["2026-10-15", "PAY-2026-0210", "2210-001", "EPF Payable", "Employer 13%", "", "2418.00"],
[
"2026-10-15",
"PAY-2026-0210",
"2220-001",
"SOCSO / EIS Payable",
"Employer share",
"",
"412.30",
],
[
"2026-10-15",
"PAY-2026-0210",
"1000-001",
"Maybank Current Account",
"Net salaries",
"",
"15769.70",
],
["2026-10-28", "JV-2026-0311", "6500-001", "Depreciation", "Office equipment", "820.00", ""],
[
"2026-10-28",
"JV-2026-0311",
"1510-001",
"Acc. Depreciation — Equipment",
"Office equipment",
"",
"820.00",
],
[
"2026-10-31",
"JV-2026-0312",
"3000-001",
"Retained Earnings",
"Opening balance b/f",
"",
"48250.00",
],
].map((a, i) => ({
id: String(i),
date: a[0],
ref: a[1],
code: a[2],
account: a[3],
memo: a[4],
dr: a[5],
cr: a[6],
}));
const dr = sumAmounts(L.map((r) => r.dr)),
cr = sumAmounts(L.map((r) => r.cr));
const N2 = new Intl.NumberFormat("en-MY", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
function amt(v) {
return v ? <span className="tabular-nums">{N2.format(v)}</span> : null;
}
const cols = [
{ key: "date", header: "Date", nowrap: true, mobile: "meta", cell: (r) => formatDate(r.date) },
{
key: "account",
header: "Account",
nowrap: true,
mobile: "title",
cell: (r) => (
<span>
<span className="mr-2 font-mono text-xs text-tertiary-foreground">{r.code}</span>
{r.account}
</span>
),
},
{
key: "ref",
header: "Reference",
mobile: "stacked",
cell: (r) => <span className="font-mono text-xs text-primary">{r.ref}</span>,
},
{
key: "memo",
header: "Description",
mobile: "meta",
className: "w-full max-w-0",
cell: (r) => (
<span className="block truncate text-muted-foreground" title={r.ref + " · " + r.memo}>
{r.memo}
</span>
),
},
{ key: "dr", header: "Debit (RM)", align: "right", mobile: "meta", cell: (r) => amt(r.dr) },
{ key: "cr", header: "Credit (RM)", align: "right", mobile: "meta", cell: (r) => amt(r.cr) },
];
export default function DensityToggleDemo() {
const [density, setDensity] = useState("compact");
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<div className="text-md font-semibold text-foreground">General ledger · October 2026</div>
<div className="text-xs text-tertiary-foreground">
Try: Tab to the toggle, ← → to change density; scroll the list — header and totals stay
put
</div>
</div>
<DensityToggle value={density} onValueChange={setDensity} />
</div>
<DensityProvider density={density}>
<DataList
columns={cols}
rows={L}
getKey={(r) => r.id}
caption="General ledger lines"
stickyHeader
maxHeight="16rem"
totals={{ label: "Total", dr: amt(dr), cr: amt(cr) }}
/>
</DensityProvider>
</div>
);
} Use
In a list toolbar. Wrap the lists it controls in <DensityProvider density={value}>; every DataList inside picks it up. Pass storageKey to remember the choice per user.
Props
value / defaultValue ("compact" | "default" | "comfortable", default "default") / onValueChange, labels (auto — hide words when the nearest @container is under 22rem; always; never), storageKey (localStorage, uncontrolled use), label (group name, default "Row density"), className and any div props.
Notes
The demo DataList uses stickyHeader + maxHeight="16rem" so the header and the totals row stay in view while lines scroll; hidden in print.