Pass groupBy with a row key or callback to insert category headers in the standard paginated table. Grouping is intentionally ignored when virtualScroll is enabled.
Use fillParent when the surrounding shell already owns height. In flex layouts, make the table a direct flex child and keep shrinking wrappers at min-height: 0 so the internal scroll viewport can resolve correctly.
Virtual mode keeps native scrolling for touch devices, incrementally fetches matching pages, and preserves search, sort, and external filters in every request. Use virtualScrollHeight when the table owns its own viewport, or combine virtualScroll with fillParent inside a bounded flex shell.
Use this checklist before enabling virtualScroll in production. The feature is intentionally query-driven so the same loadData contract works for both pagination and continuous scrolling.
What virtual mode changes
Pagination stays the default. Nothing changes until you pass virtualScroll.
The table still calls loadData(query) with page, pageSize, search, sort, and filters.
In virtual mode, pageSize becomes the fetch batch size rather than a single visible page.
The DOM only renders the visible row window plus overscan, so large datasets stay responsive.
What stays preserved
Search remains built in and still debounces before refetching.
Sorting still resets to the top of the result set and refetches with the new order.
External filters continue to flow through query.filters on every request, even while the table is fetching additional pages during scroll.
Custom cells, row actions, row click handlers, and row-level loading styles still work the same way.
Layout and iPad guidance
Give the table a bounded viewport with fillParent or virtualScrollHeight. Virtual mode needs an actual scroll container.
If you use fillParent in a flex dashboard, keep each shrinking ancestor at min-height: 0 so the table can resolve a non-zero viewport height.
The scroll container uses native overflow scrolling with momentum, which is what you want on iPad instead of a custom gesture layer.
Keep virtualRowHeight close to the real rendered row height. If the estimate is too small or too large, the scroll window will feel less accurate.
Increase virtualOverscan when touch users scroll quickly and you want more rows pre-rendered ahead of the viewport.
Recommended implementation checklist
Return a stable total from loadData whenever possible so the footer and load-more logic stay accurate.
Apply filtering and sorting on the same query path the table already uses for pagination. Do not create a second code path just for virtual mode.
If the table renders but never scrolls or keeps loading, inspect the layout chain first. Virtual prefetch only starts once the viewport has a real height.
Use a batch size that matches the density of the screen. Dense admin tables can often use 75 or 100; taller mobile cards may prefer smaller batches.
Test on a real touch device before shipping. Fast momentum scrolling is where overscan and row-height estimates matter most.
Row-Level Update Indicator Demo
Rows where cwloading === true are highlighted and blurred, while the rest of the table stays visible.
Use a row convention to trigger update state styling. The datatable checks each row with a strict boolean comparison.
Set cwloading: true on a row object to show the yellow background + blur indicator.
Set cwloading to false, null, undefined, or remove it to restore normal row styles.
For async row updates, set cwloading before the request and clear it after the request resolves.
Only the affected rows change state; the table dataset remains visible during the async update.
Documentation Upgrade
Start here
CwDataTable is query-driven. The table owns search, sort, pagination, refresh, column visibility, optional grouped rows, and virtual windowing, while your `loadData(query)` function stays authoritative for rows, totals, and filters. Built-in column settings persist visible columns per grid ID in local storage.
How to think about it
Start from the query contractEvery interaction funnels back through `loadData(query)`. Search text, sort state, `page`, `pageSize`, and `filters` are all part of the same request shape.
Use pagination by defaultKeep the default mode when the dataset is moderate or when you want explicit page navigation. Turn on `virtualScroll` when the main goal is continuous browsing through large result sets.
Group rows only in paginated modeUse `groupBy` when you want visual category headers inside the default paginated table. Group headers are intentionally skipped in `virtualScroll` mode so row windowing stays predictable.
Use fillParent inside a bounded flex shellIf a dashboard card or split pane already owns height, make the table a direct flex child with `fillParent` and keep shrinking ancestors at `min-height: 0` so the internal viewport can resolve.
Use virtualScrollHeight when the table owns heightWhen you do not control the parent layout, set `virtualScrollHeight` and let the table manage its own viewport. This is the simplest standalone pattern.
Treat filters as external stateSearch and sort UI are built in. Filters are intentionally passed in as `Record<string, string[]>` so your page owns filter controls while the table still preserves them in every request.
Fix layout before debugging data loadingVirtual mode only prefetches once the viewport has a real height. If the table renders but does not continue loading on scroll, inspect the layout chain first.
Tune for touch, not just desktop`virtualRowHeight` and `virtualOverscan` matter most on iPad and other touch devices. Keep row height estimates honest and overscan high enough to avoid blank gaps during momentum scrolling.
Core API for pagination, grouping, and virtual mode
Pagination is the default behavior. Use `groupBy` there when you want section headers between rows. In virtual mode, `pageSize` becomes the number of rows fetched per request rather than the number of rows shown on one discrete page, and grouped headers are disabled to keep the virtual window stable. Every table also includes a built-in overflow menu for refresh and persisted column settings. Pair `virtualScroll` with either `virtualScrollHeight` or `fillParent` so the table has a real scroll viewport.
API
Type
Details
columnsRequired
CwColumnDef<T>[]
Column definitions for headers, alignment, sortability, and optional string cell formatters.
Async row loader. Receives `page`, `pageSize`, `search`, `sort`, `filters`, and `signal`, and should return the matching `rows` plus `total` when known.
rowKeyRequired
keyof T & string
Stable unique key used for keyed row rendering and row actions.
searchable
Default: true
boolean
Shows the built-in search input in the toolbar.
filters
Default: {}
Record<string, string[]>
Externally-controlled filters preserved in every request. Use this when your page owns dropdowns, chips, tabs, or other filter controls.
pageSize
Default: 20
number
Rows per page in pagination mode. In virtual mode, this becomes the batch size requested each time the table fetches more rows.
pageSizeOptions
Default: [10, 20, 50, 100]
number[]
Toolbar choices shown in the rows-per-page or rows-per-batch dropdown.
groupBy
keyof T & string | ((row: T) => string | number | boolean | null | undefined)
Optional grouping key or callback for the default paginated renderer. The table inserts group headers for each resolved label and ignores this prop in `virtualScroll` mode.
ungroupedLabel
Default: 'Ungrouped'
string
Fallback group header label used when `groupBy` resolves to `null`, `undefined`, or an empty string.
gridId
Default: derived from route
string
Storage key for persisted column visibility. Defaults to the current page path plus `_grid`; pass an explicit value when multiple tables share one route.
virtualScroll
Default: false
boolean
Enables continuous scrolling. The table incrementally loads pages, renders only the visible row window plus overscan, and waits for a real viewport before prefetching more rows.
virtualScrollHeight
Default: '28rem'
string
Viewport height used when `virtualScroll` is enabled without `fillParent`. Choose this when the table should manage its own standalone scroll area. Accepts any CSS size such as `24rem`, `50vh`, or `480px`.
virtualRowHeight
Default: 52
number
Estimated row height in pixels for virtual windowing. Keep this close to the real rendered row height for the smoothest touch scrolling.
virtualOverscan
Default: 12
number
Extra rows rendered above and below the viewport. Higher values reduce visible pop-in during fast scrolls and iPad momentum scrolling.
fillParent
Default: false
boolean
Makes the table flex to fill a bounded parent and turns the body region into the scroll container. Best for dashboard cards, split panes, and other flex layouts that already define height. Any shrinking ancestor should also allow it with `min-height: 0`.
actionsHeader
Snippet
Snippet rendered in the toolbar before the built-in overflow menu. Use this for add, export, filter, or other table-level actions.
toolbarActions
Snippet
Deprecated alias for `actionsHeader`, kept so existing toolbar content keeps rendering during migration.
cell
Snippet<[T, CwColumnDef<T>, string]>
Optional custom cell renderer. Useful for badges, durations, links, and richer markup while still falling back to the table’s default string value.
rowActions
Snippet<[T]>
Optional per-row actions column rendered on the far right.
rowActionsHeader
string
Optional text label for the row actions column header. If omitted, the header stays blank.
rowTextSizeKey
string
Optional row property name containing a CSS font-size value when a dataset needs row-level text scaling.
onSearch / onSort / onPageSizeChanged / onRefresh
callbacks
Optional observers for analytics, syncing external controls, or reacting to table query changes outside the table itself. `onRefresh` fires when the built-in Refresh menu action is selected.
Copy-paste examples
These snippets intentionally show the full public API surface the live demo relies on.
Basic paginated table
Use this first. Search and sort are built in, while your loader stays responsible for filtering, slicing, and total counts.
Use this when the table owns the viewport height with `virtualScrollHeight`, while the page still owns filter state across search, sort, and scrolling.
Fill-parent virtual table inside a flex dashboard shell
Prefer this layout when the surrounding shell already defines height. The key is making the table a flex child and keeping each shrinking wrapper at `min-height: 0`.
Fill-parent virtual table inside a flex dashboard shellCopy code