Saved views
ProA bar of saved views over a table or report. Picking a view restores its filters, sorting and columns; changing them marks the view modified, with Save, Save as new and Reset.
npx prodular-ui add saved-viewsPart of Prodular UI Pro. Preview and read the docs freely, install with a license. See pricing
Views over a data table
Two locked presets, a shared view and a personal one. Change a filter or sort to see Modified, then save it or save it as a new view.
| INV-2109 | Bluefin Marine | Draft | Theo Lindqvist | Nov 14 | $9,061 |
| INV-2116 | Northwind Logistics | Paid | Maya Okafor | Nov 13 | $8,944 |
| INV-2123 | Kestrel Health | Paid | Hana Sato | Nov 12 | $8,827 |
| INV-2130 | Halcyon Robotics | Paid | Priya Raman | Nov 11 | $8,710 |
| INV-2137 | Meridian Freight | Open | Theo Lindqvist | Nov 10 | $8,593 |
| INV-2106 | Halcyon Robotics | Paid | Priya Raman | Nov 8 | $17,254 |
| INV-2113 | Meridian Freight | Open | Theo Lindqvist | Nov 7 | $17,137 |
| INV-2120 | Lumen Analytics | Open | Maya Okafor | Nov 6 | $17,020 |
1–8 of 36
"use client"
import * as React from "react"import type { ColumnFiltersState, ColumnVisibilityState, SortingState } from "@tanstack/react-table"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"import { DataTable, DataTableColumnHeader, DataTableContent, DataTableFilterBar, DataTableFilterChips, DataTablePagination, DataTableSearch, DataTableToolbar, DataTableToolbarSpacer, DataTableViewOptions, createDataTableColumnHelper, useDataTable, type DataTableFilterConfig,} from "@/components/ui/data-table"import { SavedViews, useSavedViews, type SavedView } from "@/components/ui/saved-views"import { Status, StatusIndicator, type StatusVariant } from "@/components/ui/status"
type InvoiceStatus = "paid" | "open" | "overdue" | "draft"type Invoice = { id: string; customer: string; status: InvoiceStatus; owner: string; due: Date; amount: number }
const owners = [ { name: "Maya Okafor", initials: "MO" }, { name: "Theo Lindqvist", initials: "TL" }, { name: "Priya Raman", initials: "PR" }, { name: "Hana Sato", initials: "HS" },]const customers = ["Northwind Logistics", "Kestrel Health", "Halcyon Robotics", "Meridian Freight", "Lumen Analytics", "Pinecrest Schools", "Tidewater Energy", "Fable & Finch", "Ironclad Security", "Orchard Lane Grocers", "Arcwell Medical", "Bluefin Marine"]const statuses: InvoiceStatus[] = ["paid", "open", "overdue", "paid", "open", "draft", "paid", "overdue"]
const invoices: Invoice[] = Array.from({ length: 36 }, (_, i) => ({ id: `INV-${2140 - i}`, customer: customers[(i * 5) % customers.length], status: statuses[(i * 3) % statuses.length], owner: owners[(i * 7) % owners.length].name, due: new Date(2026, 9, 2 + ((i * 13) % 45)), amount: 400 + ((i * 2731) % 19000),}))
const statusMeta: Record<InvoiceStatus, { label: string; variant: StatusVariant }> = { paid: { label: "Paid", variant: "success" }, open: { label: "Open", variant: "info" }, overdue: { label: "Overdue", variant: "destructive" }, draft: { label: "Draft", variant: "neutral" },}
const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 })const shortDate = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric" })const helper = createDataTableColumnHelper<Invoice>()
const columns = helper.columns([ helper.accessor("id", { header: ({ column }) => <DataTableColumnHeader column={column} title="Invoice" />, meta: { label: "Invoice" }, cell: ({ getValue }) => <span className="font-mono text-[13px]">{getValue()}</span>, }), helper.accessor("customer", { header: ({ column }) => <DataTableColumnHeader column={column} title="Customer" />, meta: { label: "Customer" }, cell: ({ getValue }) => <span className="font-medium">{getValue()}</span>, }), helper.accessor("status", { header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />, meta: { label: "Status" }, filterFn: "facet", cell: ({ getValue }) => ( <Status variant={statusMeta[getValue()].variant} appearance="pill"> {statusMeta[getValue()].label} </Status> ), }), helper.accessor("owner", { header: ({ column }) => <DataTableColumnHeader column={column} title="Owner" />, meta: { label: "Owner" }, filterFn: "facet", cell: ({ getValue }) => <span className="text-muted-foreground">{getValue()}</span>, }), helper.accessor("due", { header: ({ column }) => <DataTableColumnHeader column={column} title="Due" />, meta: { label: "Due date", align: "end" }, sortFn: "datetime", enableGlobalFilter: false, cell: ({ getValue }) => <span className="text-muted-foreground">{shortDate.format(getValue())}</span>, }), helper.accessor("amount", { header: ({ column }) => <DataTableColumnHeader column={column} title="Amount" />, meta: { label: "Amount", align: "end" }, enableGlobalFilter: false, cell: ({ getValue }) => <span className="font-medium">{usd.format(getValue())}</span>, }),])
const filters: DataTableFilterConfig[] = [ { column: "status", title: "Status", pinned: true, options: (Object.keys(statusMeta) as InvoiceStatus[]).map((s) => ({ value: s, label: statusMeta[s].label, icon: <StatusIndicator variant={statusMeta[s].variant} /> })), }, { column: "owner", title: "Owner", pinned: true, options: owners.map((o) => ({ value: o.name, label: o.name, icon: ( <Avatar size="sm"> <AvatarFallback>{o.initials}</AvatarFallback> </Avatar> ), })), },]
/** What a view restores. Plain JSON, so it can be saved anywhere. */type ViewState = { search: string; filters: ColumnFiltersState; sorting: SortingState; columns: ColumnVisibilityState }
const presets: SavedView<ViewState>[] = [ { id: "all", name: "All invoices", locked: true, state: { search: "", filters: [], sorting: [{ id: "due", desc: true }], columns: {} } }, { id: "overdue", name: "Overdue", locked: true, state: { search: "", filters: [{ id: "status", value: ["overdue"] }], sorting: [{ id: "due", desc: false }], columns: {} } }, { id: "large-open", name: "Large and open", shared: true, state: { search: "", filters: [{ id: "status", value: ["open", "overdue"] }], sorting: [{ id: "amount", desc: true }], columns: { owner: false } }, }, { id: "maya", name: "Maya's accounts", state: { search: "", filters: [{ id: "owner", value: ["Maya Okafor"] }], sorting: [{ id: "due", desc: true }], columns: {} } },]
export default function SavedViewsInvoices() { const { views, setViews, activeId, setActiveId } = useSavedViews({ initialViews: presets, initialActiveId: "all" }) const table = useDataTable({ data: invoices, columns, getRowId: (row) => row.id, initialState: { sorting: presets[0].state.sorting, pagination: { pageIndex: 0, pageSize: 8 } }, })
const state: ViewState = { search: (table.state.globalFilter as string | undefined) ?? "", filters: table.state.columnFilters, sorting: table.state.sorting, columns: table.state.columnVisibility, }
const apply = (id: string, view: SavedView<ViewState>) => { setActiveId(id) table.setGlobalFilter(view.state.search) table.setColumnFilters(view.state.filters) table.setSorting(view.state.sorting) table.setColumnVisibility(view.state.columns) table.setPageIndex(0) }
return ( <DataTable table={table}> <SavedViews views={views} onViewsChange={setViews} activeId={activeId} onActiveChange={apply} state={state} shareable /> <DataTableToolbar> <DataTableSearch placeholder="Search invoices" /> <DataTableFilterBar filters={filters} /> <DataTableToolbarSpacer /> <DataTableViewOptions /> </DataTableToolbar> <DataTableFilterChips filters={filters} /> <DataTableContent /> <DataTablePagination pageSizes={[8, 20, 50]} /> </DataTable> )}Anatomy
const { views, setViews, activeId, setActiveId } = useSavedViews({ initialViews })
<SavedViews views={views} onViewsChange={setViews} activeId={activeId} onActiveChange={(id, view) => { setActiveId(id); applyToTable(view.state) }} state={currentState}/>Guidance
Use it when
- Tables, boards and reports that people revisit with the same filters, like Overdue or My accounts.
- Locked presets for the views every team needs, and personal or shared views on top.
- The same state object for the URL, so a view can be linked and shared.
Avoid
- One-off filtering. The filter bar and chips are enough until people repeat themselves.
- More than about eight views in the bar. Keep the rest behind search or a menu.
- Saving row selection or scroll position. Views hold how to look at data, not where you were.
Rules for AI agents
Published in llms-full.txt, the registry and the MCP server, so coding agents follow them when they build with this component.
- Define a ViewState type with everything a view restores (search, filters, sorting, column visibility, grouping) and keep it JSON.
- Build the current state from the table on every render and pass it as state. SavedViews compares it with the active view to show Modified.
- In onActiveChange, set the active id and apply view.state to the table: setGlobalFilter, setColumnFilters, setSorting, setColumnVisibility, then go to the first page.
- Start the table with the default view's state, or it opens as modified.
- Mark built-in presets locked. They can be duplicated but not renamed, saved over or deleted.
- useSavedViews keeps views in React state, or in localStorage with storageKey. For shared views, load and save them through your API with onViewsChange.
API
SavedViews
| Prop | Type | Default | Description |
|---|---|---|---|
| views / onViewsChange | SavedView<T>[] / (views) => void | Views: { id, name, state, shared?, locked? }. | |
| activeId / onActiveChange | string / (id, view) => void | The selected view. Apply view.state in the callback. | |
| state | T | The current state, compared with the active view. | |
| shareable | boolean | false | Adds Share with team when saving and in the view menu. |
| isEqual | (a: T, b: T) => boolean | Custom comparison. The default ignores key order. |
useSavedViews
| Prop | Type | Default | Description |
|---|---|---|---|
| ({ initialViews, initialActiveId?, storageKey? }) | { views, setViews, activeId, setActiveId, activeView } | View state, optionally persisted in localStorage. |
Installation
The Prodular UI CLI adds the source to components/ui with everything it depends on. In a project that already uses shadcn/ui, the shadcn CLI installs it from the Prodular registry too. See Installation for the first setup.
npx prodular-ui add saved-views# or, with the registry in components.jsonnpx shadcn@latest add @prodular/saved-views