Cal.comdesign-system

Tables

How Cal.com lists, filters, sorts, and paginates tabular data with Coss UI and TanStack Table.

List and settings pages in Cal.com use a Cal-specific data-table module on top of TanStack Table and Coss table primitives — not legacy @calcom/ui DataTable.

Infrastructure lives in apps/web/modules/data-table/. Filter types and serializers live in packages/features/data-table/.

Primitive Table APIs are on coss.com/ui. This page covers product compositions.

Stack

Typical list page:

Legacy @calcom/ui DataTable is not used in current apps/web code. Do not introduce it.

List page (most common)

Admin and settings lists combine a toolbar above a card table. Toolbar layout matches DataTableToolbar:

  1. Search on the left (SearchField, sm, max width ~200px)
  2. Column visibility + Filter add on the right
  3. Active filter chips on a second row (FilterChipShell chips + FilterBarActions for Clear / Save segment)
List page · toolbar + card table
Role
Rows per page
1-5 of 6
apps/web — data table toolbar (pattern)

Related atom docs: Column visibility menu, Filter add menu, Filter chip shell, Filter bar actions.

Real call sites: UserListTable.tsx, MemberList.tsx, AbuseRulesView.tsx, BlockedEntriesTable.tsx.

Card table shell

Interactive tables render inside CardFrame with Table variant="card" and horizontal scroll via ScrollArea. Sortable columns use clickable header cells with chevron direction — built into DataTable, not a separate component.

apps/web — data-table.tsx (pattern)

Card backdrop overrides are elided above: data-table.tsx also passes a dimmed --border on Table and a matching before:bg on CardFrame. Without them the card-variant table draws its rounded outline at full border weight and the footer detaches into a separate block below — copy them verbatim from data-table.tsx.

Pagination sits in CardFrameFooter — page size Select on the left, range label + prev/next on the right (DataTablePagination). Cursor pagination uses the same footer with a page counter instead of total count.

Row actions live in column defs (menu button in an actions column). Row click opens a drawer with details when onRowMouseclick is set — ignore clicks on buttons/links inside the row.

Column resize and pinned columns are supported in DataTable for wide admin tables (members, blocklist).

Empty states

Two patterns — see also Empty states.

In-card empty (filters stay mounted)

When filters or date range sit above the table, nest Empty in CardPanel without an extra dashed border. Keeps loading → empty → data transitions stable.

In-card empty · date filter stays visible
No invoices found
No invoices in this date range.

Used in InvoicesTable.tsx, filterable list pages via DataTableWrapper + EmptyView.

Full-page empty with CTA

When the whole list is empty (no rows at all), pass a presentational Empty with EmptyContent as EmptyView to DataTableWrapper — same as blocklist / attributes list empties.

DataTable alone falls back to a centered “No results” cell when the wrapper does not supply EmptyView and TanStack returns zero rows after filters.

Loading

Prefer row skeletons while refetching so headers, toolbar, and footer stay mounted:

apps/web — BlockedEntriesTable (pattern)

Match skeleton shape per column (Skeleton widths per columnId) — not one generic bar per row.

For first paint with no chrome yet, loading.mode: "table" swaps in a full DataTableSkeleton.

Loading · row skeletons
NameEmailRole

Simple static table

Small, non-interactive lists can use plain Table (default variant) without TanStack or CardFrame — no sort, filter, or pagination chrome.

Simple table · small static list
ProviderStatusTimestampActions
Google CalendarSuccess2 minutes ago
OutlookFailed1 hour ago

Real call site: integration-sync-runs-table.tsx.

Bespoke tables

Most product tables go through DataTable + DataTableWrapper. blocklist-table.tsx builds TanStack + Coss table directly and uses TableSortableHeaderButton + ColumnVisibilityMenu in the header row — reference when you cannot use the shared module (tabs, custom layout).

Bookings is another exception: TanStack state for filters/sort/pagination, but rows render as ListItem inside CardFrame instead of <table> rows.

Do / don't

Do

  • Use apps/web/modules/data-table/ for new filterable list pages
  • Wrap interactive tables in CardFrame + Table variant="card"
  • Keep toolbar + filter bar mounted across loading / empty / data states
  • Use @coss/ui/components/empty for meaningful empties (with CTA when the user can create)
  • Use per-column skeleton cells during row loading

Don't

  • Add legacy @calcom/ui DataTable
  • Drop filter chrome when showing an in-card empty
  • Use TableSortableHeaderButton in standard list pages — DataTable header sort is the default
  • Show a generic “No results” cell when a full Empty with next steps is appropriate

Reference call sites