SavedViewsBar
Saved views for any list: a scrolling row of view tabs with counts, the active filters as removable FilterChips, and Save / Save as new / Reset once the filters drift from the view; rename, duplicate, share and delete from the active view's menu.
import { SavedViewsBar } from "@cynco-labs/ui"; Example
import { Fragment, useState } from "react";
import { DotsThreeIcon, PlusIcon } from "@phosphor-icons/react";
import {
Button,
DataList,
DocStatus,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
INVOICE_STATUS,
Kbd,
Money,
SavedViewsBar,
formatDate,
sumAmounts,
} from "@cynco-labs/ui";
const TODAY = "2026-11-01";
const rows = [
{
id: "1",
no: "INV-2026-0142",
customer: "Syarikat Maju Jaya Sdn Bhd",
due: "2026-11-04",
amount: "12850.00",
status: "finalized",
owner: "me",
},
{
id: "2",
no: "INV-2026-0140",
customer: "Tropika Kopi Enterprise",
due: "2026-10-17",
amount: "3240.00",
status: "overdue",
owner: "aisyah",
},
{
id: "3",
no: "INV-2026-0139",
customer: "Nusantara Logistik Bhd",
due: "2026-10-30",
amount: "28400.00",
status: "paid",
owner: "me",
},
{
id: "4",
no: "INV-2026-0143",
customer: "Kedai Runcit Ah Seng",
due: "2026-11-15",
amount: "860.00",
status: "draft",
owner: "me",
},
{
id: "5",
no: "INV-2026-0137",
customer: "Syarikat Maju Jaya Sdn Bhd",
due: "2026-10-09",
amount: "15600.00",
status: "overdue",
owner: "me",
},
{
id: "6",
no: "INV-2026-0144",
customer: "Borneo Timber Works Sdn Bhd",
due: "2026-11-06",
amount: "7420.50",
status: "finalized",
owner: "aisyah",
},
{
id: "7",
no: "INV-2026-0145",
customer: "Tropika Kopi Enterprise",
due: "2026-11-30",
amount: "1980.00",
status: "draft",
owner: "aisyah",
},
{
id: "8",
no: "INV-2026-0141",
customer: "Nusantara Logistik Bhd",
due: "2026-11-03",
amount: "22150.00",
status: "partially_paid",
owner: "me",
},
];
const STATUS_LABEL = {
draft: "Draft",
finalized: "Pending",
overdue: "Overdue",
paid: "Paid",
partially_paid: "Partially Paid",
};
function match(r, f) {
if (f.field === "status") return STATUS_LABEL[r.status] === f.value;
if (f.field === "customer") return r.customer === f.value;
if (f.field === "owner") return f.value === "Me" ? r.owner === "me" : true;
if (f.field === "due")
return f.value === "This week"
? r.due >= TODAY && r.due <= "2026-11-07" && r.status !== "paid"
: true;
if (f.field === "amount") return Number(r.amount) >= 10000;
return true;
}
function apply(fs) {
const byField = {};
fs.forEach((f) => {
(byField[f.field] = byField[f.field] || []).push(f);
});
return rows.filter((r) => Object.keys(byField).every((k) => byField[k].some((f) => match(r, f))));
}
const VIEWS = [
{ id: "all", name: "All", filters: [], locked: true },
{
id: "overdue",
name: "Overdue",
filters: [{ id: "f1", field: "status", label: "Status", value: "Overdue" }],
shared: true,
},
{
id: "week",
name: "Due this week",
filters: [{ id: "f2", field: "due", label: "Due", value: "This week" }],
shared: true,
},
{
id: "drafts",
name: "My drafts",
filters: [
{ id: "f3", field: "status", label: "Status", value: "Draft" },
{ id: "f4", field: "owner", label: "Owner", value: "Me" },
],
},
{
id: "big",
name: "Maju Jaya ≥ RM 10k",
filters: [
{ id: "f5", field: "customer", label: "Customer", value: "Syarikat Maju Jaya Sdn Bhd" },
{ id: "f6", field: "amount", label: "Amount", value: "≥ RM 10,000" },
],
},
];
const ADD = [
{
field: "status",
label: "Status",
values: ["Draft", "Pending", "Overdue", "Paid", "Partially Paid"],
},
{
field: "customer",
label: "Customer",
values: ["Syarikat Maju Jaya Sdn Bhd", "Tropika Kopi Enterprise", "Nusantara Logistik Bhd"],
},
{ field: "due", label: "Due", values: ["This week"] },
{ field: "amount", label: "Amount", values: ["≥ RM 10,000"] },
];
const cols = [
{
key: "no",
header: "Invoice #",
mobile: "title",
nowrap: true,
cell: (r) => <span className="font-mono text-xs text-primary">{r.no}</span>,
},
{ key: "customer", header: "Customer", cell: (r) => r.customer },
{ key: "due", header: "Due", nowrap: true, cell: (r) => formatDate(r.due) },
{
key: "status",
header: "Status",
mobile: "badge",
cell: (r) => <DocStatus status={r.status} config={INVOICE_STATUS} />,
},
{
key: "amount",
header: "Amount",
align: "right",
mobile: "value",
cell: (r) => <Money amount={r.amount} />,
},
];
let seq = 10;
export default function SavedViewsBarDemo() {
const [views, setViews] = useState(VIEWS);
const [active, setActive] = useState("overdue");
const [filters, setFilters] = useState(VIEWS[1].filters);
const counts = {};
views.forEach((x) => {
counts[x.id] = apply(x.filters).length;
});
const shown = apply(filters);
function add(field, label, value) {
if (filters.some((x) => x.field === field && x.value === value)) return;
setFilters(filters.concat({ id: "f" + seq++, field, label, value }));
}
const addFilter = (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="dotted" size="sm" />}>
<PlusIcon className="size-3" />
Add filter
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="min-w-56">
{ADD.map((g, gi) => (
<Fragment key={g.field}>
{gi > 0 && <DropdownMenuSeparator />}
<DropdownMenuGroup>
<DropdownMenuLabel>{g.label}</DropdownMenuLabel>
{g.values.map((val) => (
<DropdownMenuItem
key={val}
onClick={() => {
add(g.field, g.label, val);
}}
>
{val}
</DropdownMenuItem>
))}
</DropdownMenuGroup>
</Fragment>
))}
</DropdownMenuContent>
</DropdownMenu>
);
return (
<div className="flex flex-col gap-3">
<p className="text-xs text-tertiary-foreground">
{"Try: Tab to the views, "}
<Kbd keys={["left"]} /> <Kbd keys={["right"]} />
{" then "}
<Kbd keys="enter" />
{"; remove a chip or add a filter, then Save view / Save as new / Reset. The "}
<DotsThreeIcon className="inline size-3" />
{" menu renames, shares or deletes."}
</p>
<SavedViewsBar
views={views}
onViewsChange={setViews}
activeViewId={active}
onActiveViewChange={setActive}
filters={filters}
onFiltersChange={setFilters}
counts={counts}
addFilter={addFilter}
/>
<DataList
columns={cols}
rows={shown}
getKey={(r) => r.id}
caption="Invoices"
onRowClick={() => {}}
empty="No invoices match these filters."
totals={
shown.length
? {
label: shown.length + " invoice" + (shown.length === 1 ? "" : "s"),
amount: <Money amount={sumAmounts(shown.map((r) => r.amount))} />,
}
: undefined
}
/>
</div>
);
} Use
Above a DataList whose rows you filter from filters. Ship built-in views (All, Overdue, Due this week) as locked, let people save their own as personal or shared with the team, and persist views through onViewsChange.
Props
views / defaultViews / onViewsChange (SavedView: id, name, filters, count, shared, locked); activeViewId / defaultActiveViewId / onActiveViewChange; filters / defaultFilters / onFiltersChange (SavedViewFilter: id, field, label, value — the list applies field+value, chips show label and value); counts (rows per view id); addFilter (your Add-filter control, rendered after the chips); label (accessible name, default “Saved views”); className and any div props.
Notes
A blue dot marks a modified view; locked views offer Save as new only. The tab row scrolls sideways on narrow containers and the save actions wrap under the chips.