Repository: calcom/cal.com
Pull request #21147: feat(v3): table designs
Author: login: sean-brydon
Labels: name: ✨ feature
description: New feature or request
name: ui
description: area: UI, frontend, button, form, input
name: core
description: area: core, team members only
name: consumer
name: ready-for-e2e
Created: 2025-05-07T09:25:38Z | Merged: 2025-05-07T09:54:36Z

## What does this PR do?
Adds V3 inital designs to data-table. Bookings page V3 designs will come in a follow up PR

![CleanShot 2025-05-07 at 10 23 15](https://github.com/user-attachments/assets/a1406fde-4214-4858-9265-e6a607332650)
![CleanShot 2025-05-07 at 10 23 29](https://github.com/user-attachments/assets/7ff2443e-ec9b-4ab7-9280-6cc17bb2be34)
![CleanShot 2025-05-07 at 10 23 40](https://github.com/user-attachments/assets/21dc66e9-46ad-426a-8d08-1d21c6f31653)

    
<!-- This is an auto-generated description by mrge. -->
---

## Summary by mrge
Updated the data table with new V3 designs, including refreshed styles, improved layout, and updated toolbar for a cleaner look.

- **UI Improvements**
  - Added rounded corners, muted backgrounds, and better spacing.
  - Updated toolbar layout and filter controls.
  - Improved row and header styling for consistency.

<!-- End of auto-generated description by mrge. -->




Diff:
diff --git a/packages/features/data-table/components/DataTable.tsx b/packages/features/data-table/components/DataTable.tsx
index 08f666c586fe9e..1d882c9d15ba14 100644
--- a/packages/features/data-table/components/DataTable.tsx
+++ b/packages/features/data-table/components/DataTable.tsx
@@ -39,6 +39,7 @@ export type DataTablePropsFromWrapper<TData> = {
   headerClassName?: string;
   rowClassName?: string;
   paginationMode?: "infinite" | "standard";
+  hasWrapperContext?: boolean;
 };
 
 export type DataTableProps<TData> = DataTablePropsFromWrapper<TData> & {
@@ -64,6 +65,7 @@ export function DataTable<TData>({
   headerClassName,
   rowClassName,
   paginationMode = "infinite",
+  hasWrapperContext = false,
   ...rest
 }: DataTableProps<TData> & React.ComponentPropsWithoutRef<"div">) {
   const { rows } = table.getRowModel();
@@ -104,7 +106,10 @@ export function DataTable<TData>({
 
   return (
     <div
-      className={classNames("grid", className)}
+      className={classNames(
+        !hasWrapperContext ? "grid" : "bg-muted grid rounded-xl px-0.5 pb-0.5",
+        className
+      )}
       style={{
         gridTemplateRows: "auto 1fr auto",
         gridTemplateAreas: "'header' 'body' 'footer'",
@@ -129,20 +134,23 @@ export function DataTable<TData>({
         onScroll={onScroll}
         className={classNames(
           "relative overflow-auto",
-          "scrollbar-thin border-subtle relative rounded-md border",
+          "scrollbar-thin relative rounded-md ",
           paginationMode === "infinite" && "h-[80dvh]", // Set a fixed height for the container
           containerClassName
         )}
         style={{ gridArea: "body" }}>
         <TableNew
-          className="data-table grid border-0"
+          className={classNames(
+            "data-table grid border-0",
+            !hasWrapperContext && "bg-muted rounded-xl px-0.5 pb-0.5"
+          )}
           style={{
             ...columnSizingVars,
             ...(Boolean(enableColumnResizing) && { width: table.getTotalSize() }),
           }}>
           <TableHeader className={classNames("sticky top-0 z-10", headerClassName)}>
             {table.getHeaderGroups().map((headerGroup: HeaderGroup<TData>) => (
-              <TableRow key={headerGroup.id} className="hover:bg-subtle flex w-full">
+              <TableRow key={headerGroup.id} className="hover:bg-subtle flex w-full border-none">
                 {headerGroup.headers.map((header: Header<TData, unknown>) => {
                   const { column } = header;
                   return (
@@ -155,7 +163,6 @@ export function DataTable<TData>({
                       }}
                       className={classNames(
                         "relative flex shrink-0 items-center",
-                        "bg-subtle",
                         column.getIsPinned() && "top-0 z-20 sm:sticky"
                       )}>
                       <TableHeadLabel header={header} />
@@ -164,7 +171,7 @@ export function DataTable<TData>({
                           onMouseDown={header.getResizeHandler()}
                           onTouchStart={header.getResizeHandler()}
                           className={classNames(
-                            "group absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-[0.1] hover:opacity-50",
+                            "group absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-0 hover:opacity-50",
                             header.column.getIsResizing() && "!opacity-75"
                           )}>
                           <div className="bg-inverted mx-auto h-full w-[1px]" />
@@ -279,7 +286,10 @@ function DataTableBody<TData>({
   }
 
   return (
-    <TableBody className="relative grid" data-testid={testId} style={{ height: tableHeight }}>
+    <TableBody
+      className="border-subtle relative grid overflow-hidden rounded-xl border"
+      data-testid={testId}
+      style={{ height: tableHeight }}>
       {rowsToRender.map(({ row, virtualItem }) => (
         <TableRow
           ref={virtualItem ? (node) => rowVirtualizer.measureElement(node) : undefined}
@@ -307,7 +317,7 @@ function DataTableBody<TData>({
                   width: `var(--col-${kebabCase(cell.column.id)}-size)`,
                 }}
                 className={classNames(
-                  "flex shrink-0 items-center overflow-hidden",
+                  "bg-default group-hover:!bg-muted group-data-[state=selected]:bg-subtle flex shrink-0 items-center overflow-hidden",
                   variant === "compact" && "p-0",
                   column.getIsPinned() &&
                     "bg-default group-hover:!bg-muted group-data-[state=selected]:bg-subtle sm:sticky"
@@ -351,7 +361,7 @@ const TableHeadLabel = ({ header }: { header: Header<any, any> }) => {
             open && "bg-muted"
           )}>
           <div
-            className="truncate"
+            className="text-default truncate text-sm leading-none"
             title={
               typeof header.column.columnDef.header === "string" ? header.column.columnDef.header : undefined
             }>
diff --git a/packages/features/data-table/components/DataTableWrapper.tsx b/packages/features/data-table/components/DataTableWrapper.tsx
index be3b4777e16a71..0ac9b18e278c46 100644
--- a/packages/features/data-table/components/DataTableWrapper.tsx
+++ b/packages/features/data-table/components/DataTableWrapper.tsx
@@ -1,6 +1,6 @@
 "use client";
 
-import type { VisibilityState } from "@tanstack/react-table";
+import type { Row, VisibilityState } from "@tanstack/react-table";
 // eslint-disable-next-line no-restricted-imports
 import { noop } from "lodash";
 import { useEffect, useRef } from "react";
@@ -22,6 +22,7 @@ type BaseDataTableWrapperProps<TData> = Omit<
   EmptyView?: React.ReactNode;
   LoaderView?: React.ReactNode;
   tableContainerRef?: React.RefObject<HTMLDivElement>;
+  onRowMouseclick?: (row: Row<TData>) => void;
 };
 
 type InfinitePaginationProps<TData> = BaseDataTableWrapperProps<TData> & {
@@ -61,6 +62,7 @@ export function DataTableWrapper<TData>({
   children,
   tableContainerRef: externalRef,
   paginationMode,
+  onRowMouseclick,
 }: DataTableWrapperProps<TData>) {
   const internalRef = useRef<HTMLDivElement>(null);
   const tableContainerRef = externalRef || internalRef;
@@ -130,13 +132,15 @@ export function DataTableWrapper<TData>({
           headerClassName={headerClassName}
           rowClassName={rowClassName}
           paginationMode={paginationMode}
+          onRowMouseclick={onRowMouseclick}
+          hasWrapperContext={true}
           onScroll={
             paginationMode === "infinite"
               ? (e: Pick<React.UIEvent<HTMLDivElement, UIEvent>, "target">) =>
                   fetchMoreOnBottomReached(e.target as HTMLDivElement)
               : undefined
           }>
-          <div style={{ gridArea: "footer", marginTop: "1rem" }}>
+          <div style={{ gridArea: "footer" }} className="px-3 py-2">
             <DataTablePagination<TData>
               table={table}
               totalRowCount={totalRowCount}
diff --git a/packages/features/ee/teams/components/MemberList.tsx b/packages/features/ee/teams/components/MemberList.tsx
index 64a81aa0f507e7..e5dfb369b08304 100644
--- a/packages/features/ee/teams/components/MemberList.tsx
+++ b/packages/features/ee/teams/components/MemberList.tsx
@@ -19,10 +19,10 @@ import type { Dispatch, SetStateAction } from "react";
 import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
 import { Dialog } from "@calcom/features/components/controlled-dialog";
 import {
-  DataTable,
   DataTableProvider,
   DataTableToolbar,
   DataTableFilters,
+  DataTableWrapper,
   DataTableSelectionBar,
   useDataTable,
   useFetchMoreOnBottomReached,
@@ -305,7 +305,6 @@ function MemberListContent(props: Props) {
             checked={table.getIsAllPageRowsSelected()}
             onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
             aria-label="Select all"
-            className="translate-y-[2px]"
           />
         ),
         cell: ({ row }) => (
@@ -660,18 +659,27 @@ function MemberListContent(props: Props) {
 
   return (
     <>
-      <DataTable
+      <DataTableWrapper
         testId="team-member-list-container"
         table={table}
         tableContainerRef={tableContainerRef}
         isPending={isPending}
         enableColumnResizing={true}
-        onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
-        <DataTableToolbar.Root>
-          <div className="flex w-full gap-2">
+        paginationMode="infinite"
+        hasNextPage={hasNextPage}
+        fetchNextPage={fetchNextPage}
+        isFetching={isFetching}
+        totalRowCount={totalRowCount}
+        ToolbarLeft={
+          <>
             <DataTableToolbar.SearchBar />
-            <DataTableFilters.AddFilterButton table={table} />
             <DataTableFilters.ColumnVisibilityButton table={table} />
+            <DataTableFilters.FilterBar table={table} />
+          </>
+        }
+        ToolbarRight={
+          <>
+            <DataTableFilters.ClearFiltersButton />
             {isAdminOrOwner && (
               <DataTableToolbar.CTA
                 type="button"
@@ -682,12 +690,8 @@ function MemberListContent(props: Props) {
                 {t("add")}
               </DataTableToolbar.CTA>
             )}
-          </div>
-          <div className="flex gap-2 justify-self-start">
-            <DataTableFilters.ActiveFilters table={table} />
-          </div>
-        </DataTableToolbar.Root>
-
+          </>
+        }>
         {numberOfSelectedRows >= 2 && dynamicLinkVisible && (
           <DataTableSelectionBar.Root className="!bottom-[7.3rem] md:!bottom-32">
             <DynamicLink table={table} domain={domain} />
@@ -715,7 +719,7 @@ function MemberListContent(props: Props) {
             />
           </DataTableSelectionBar.Root>
         )}
-      </DataTable>
+      </DataTableWrapper>
       {state.deleteMember.showModal && (
         <Dialog
           open={true}
diff --git a/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx b/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx
index 8743f44f531dc9..9c6a0a1615d2a9 100644
--- a/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx
+++ b/packages/features/users/components/UserTable/PlatformManagedUsersTable.tsx
@@ -113,7 +113,6 @@ function UserListTableContent({ oAuthClientId }: PlatformManagedUsersTableProps)
             checked={table.getIsAllPageRowsSelected()}
             onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
             aria-label="Select all"
-            className="translate-y-[2px]"
           />
         ),
         cell: ({ row }) => (
diff --git a/packages/features/users/components/UserTable/UserListTable.tsx b/packages/features/users/components/UserTable/UserListTable.tsx
index 694e377f1b6d57..9e0a8a73f5d2e4 100644
--- a/packages/features/users/components/UserTable/UserListTable.tsx
+++ b/packages/features/users/components/UserTable/UserListTable.tsx
@@ -257,7 +257,6 @@ function UserListTableContent() {
             checked={table.getIsAllPageRowsSelected()}
             onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
             aria-label="Select all"
-            className="translate-y-[2px]"
           />
         ),
         cell: ({ row }) => (
diff --git a/packages/ui/components/pagination/Pagination.tsx b/packages/ui/components/pagination/Pagination.tsx
index ca99447d57c857..b5bd4b7a343800 100644
--- a/packages/ui/components/pagination/Pagination.tsx
+++ b/packages/ui/components/pagination/Pagination.tsx
@@ -68,7 +68,7 @@ export const Pagination = ({
   const endItem = Math.min(currentPage * pageSize, totalItems);
 
   return (
-    <div className="flex items-center justify-between space-x-2 py-4">
+    <div className="flex items-center justify-between space-x-2">
       <div className="flex items-center space-x-2">
         <Select
           options={pageSizeSelectOptions}
diff --git a/packages/ui/components/table/TableNew.tsx b/packages/ui/components/table/TableNew.tsx
index 9907fa350a9479..006b7e8ea3b570 100644
--- a/packages/ui/components/table/TableNew.tsx
+++ b/packages/ui/components/table/TableNew.tsx
@@ -15,11 +15,7 @@ Table.displayName = "Table";
 
 const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
   ({ className, ...props }, ref) => (
-    <thead
-      ref={ref}
-      className={classNames("[&_tr]:bg-subtle md:z-10 [&_tr]:border-b", className)}
-      {...props}
-    />
+    <thead ref={ref} className={classNames("md:z-10", className)} {...props} />
   )
 );
 TableHeader.displayName = "TableHeader";


Reviews:
user: login: sean-brydon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:27:09Z
user: login: cubic-dev-ai[bot]
body: mrge found 3 issues across 7 files. Review them in [mrge.io](https://mrge.io/pr/calcom/cal.com/pull/21147)
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T09:28:22Z
user: login: eunjae-lee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T09:31:43Z
user: login: sean-brydon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:31:52Z
user: login: sean-brydon
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:31:58Z
user: login: eunjae-lee
body: Awesome 🫶🏻
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T09:32:39Z

Review comments:
diff_hunk: @@ -39,6 +39,7 @@ export type DataTablePropsFromWrapper<TData> = {
   headerClassName?: string;
   rowClassName?: string;
   paginationMode?: "infinite" | "standard";
+  hasWrapperContext?: boolean;
path: packages/features/data-table/components/DataTable.tsx
user: login: sean-brydon
body: We need this due to the fact wrapper adds a lot of layout changes. i.e toolbar etc. So this allows us to have the same style in both useages where data-table wrapper is not used
created_at: 2025-05-07T09:27:09Z
author_association: MEMBER
diff_hunk: @@ -130,13 +132,15 @@ export function DataTableWrapper<TData>({
           headerClassName={headerClassName}
           rowClassName={rowClassName}
           paginationMode={paginationMode}
+          onRowMouseclick={onRowMouseclick}
+          hasWrapperContext={true}
           onScroll={
             paginationMode === "infinite"
               ? (e: Pick<React.UIEvent<HTMLDivElement, UIEvent>, "target">) =>
                   fetchMoreOnBottomReached(e.target as HTMLDivElement)
               : undefined
           }>
-          <div style={{ gridArea: "footer", marginTop: "1rem" }}>
+          <div style={{ gridArea: "footer" }} className="px-3 py-2">
path: packages/features/data-table/components/DataTableWrapper.tsx
user: login: cubic-dev-ai[bot]
body: <!-- metadata:{"confidence":8,"steps":[{"text":"I'll review the changed lines in the PR for any issues. Let me start by examining the code changes in `packages/features/data-table/components/DataTableWrapper.tsx`.","toolCalls":[{"args":{"pattern":"packages/features/data-table/components/DataTable.tsx"},"toolName":"listFiles"}]},{"text":"Let me try to find the DataTable component to understand how it's used:","toolCalls":[{"args":{"command":"grep -r \"export function DataTable\" packages/features/data-table/components/"},"toolName":"executeCommand"}]},{"text":"Let's look at the DataTable component to understand how the new props are used:","toolCalls":[{"args":{"command":"grep -A 20 \"export type DataTableProps\" packages/features/data-table/components/DataTable.tsx"},"toolName":"executeCommand"}]},{"text":"Let's also check how the DataTable component uses the `onRowMouseclick` prop:","toolCalls":[{"args":{"command":"grep -A 10 -B 10 \"onRowMouseclick\" packages/features/data-table/components/DataTable.tsx | grep -v \"export type\""},"toolName":"executeCommand"}]},{"text":"Let's also check the `hasWrapperContext` prop:","toolCalls":[{"args":{"command":"grep -r \"hasWrapperContext\" packages/features/data-table/components/"},"toolName":"executeCommand"}]}]} -->
Mixing inline styles with className can lead to maintainability issues. Consider using only className for styling consistency.
created_at: 2025-05-07T09:28:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -130,13 +132,15 @@
           headerClassName={headerClassName}
           rowClassName={rowClassName}
           paginationMode={paginationMode}
+          onRowMouseclick={onRowMouseclick}
+          hasWrapperContext={true}
path: packages/features/data-table/components/DataTableWrapper.tsx
user: login: cubic-dev-ai[bot]
body: <!-- metadata:{"confidence":9,"steps":[{"text":"I'll review the changed lines in the PR for any issues. Let me start by examining the code changes in `packages/features/data-table/components/DataTableWrapper.tsx`.","toolCalls":[{"args":{"pattern":"packages/features/data-table/components/DataTable.tsx"},"toolName":"listFiles"}]},{"text":"Let me try to find the DataTable component to understand how it's used:","toolCalls":[{"args":{"command":"grep -r \"export function DataTable\" packages/features/data-table/components/"},"toolName":"executeCommand"}]},{"text":"Let's look at the DataTable component to understand how the new props are used:","toolCalls":[{"args":{"command":"grep -A 20 \"export type DataTableProps\" packages/features/data-table/components/DataTable.tsx"},"toolName":"executeCommand"}]},{"text":"Let's also check how the DataTable component uses the `onRowMouseclick` prop:","toolCalls":[{"args":{"command":"grep -A 10 -B 10 \"onRowMouseclick\" packages/features/data-table/components/DataTable.tsx | grep -v \"export type\""},"toolName":"executeCommand"}]},{"text":"Let's also check the `hasWrapperContext` prop:","toolCalls":[{"args":{"command":"grep -r \"hasWrapperContext\" packages/features/data-table/components/"},"toolName":"executeCommand"}]}]} -->
Boolean props should be passed without an explicit value when possible. Use 'hasWrapperContext' instead of 'hasWrapperContext={true}'.
created_at: 2025-05-07T09:28:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -164,7 +171,7 @@ export function DataTable<TData>({
                           onMouseDown={header.getResizeHandler()}
                           onTouchStart={header.getResizeHandler()}
                           className={classNames(
-                            "group absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-[0.1] hover:opacity-50",
+                            "group absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-0 hover:opacity-50",
path: packages/features/data-table/components/DataTable.tsx
user: login: cubic-dev-ai[bot]
body: <!-- metadata:{"confidence":7,"steps":[{"text":"I'll review the changed lines in the PR for any issues. Let me analyze the code changes in `packages/features/data-table/components/DataTable.tsx`.","toolCalls":[{"args":{"pattern":"packages/features/data-table/components/**/*.tsx"},"toolName":"listFiles"}]},{"text":"Let me try a broader search to understand the structure:","toolCalls":[{"args":{"pattern":"packages/features/data-table/**"},"toolName":"listFiles"}]},{"text":"Let me check if we can find any references to the DataTable component:","toolCalls":[{"args":{"command":"grep -r \"DataTable\" --include=\"*.tsx\" ."},"toolName":"executeCommand"}]},{"text":"Now I'll analyze the code changes in the PR. Let me focus on the changes made to the DataTable component:","toolCalls":[{"args":{"command":"grep -r \"hasWrapperContext\" --include=\"*.tsx\" ."},"toolName":"executeCommand"}]}]} -->
Changed opacity from 0.1 to 0 makes resize handle completely invisible until hover
created_at: 2025-05-07T09:28:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -39,6 +39,7 @@ export type DataTablePropsFromWrapper<TData> = {
   headerClassName?: string;
   rowClassName?: string;
   paginationMode?: "infinite" | "standard";
+  hasWrapperContext?: boolean;
path: packages/features/data-table/components/DataTable.tsx
user: login: eunjae-lee
body: good addition!
created_at: 2025-05-07T09:31:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -130,13 +132,15 @@
           headerClassName={headerClassName}
           rowClassName={rowClassName}
           paginationMode={paginationMode}
+          onRowMouseclick={onRowMouseclick}
+          hasWrapperContext={true}
path: packages/features/data-table/components/DataTableWrapper.tsx
user: login: sean-brydon
body: Thats fine - we are in the wrapper 
created_at: 2025-05-07T09:31:52Z
author_association: MEMBER
diff_hunk: @@ -164,7 +171,7 @@ export function DataTable<TData>({
                           onMouseDown={header.getResizeHandler()}
                           onTouchStart={header.getResizeHandler()}
                           className={classNames(
-                            "group absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-[0.1] hover:opacity-50",
+                            "group absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-0 hover:opacity-50",
path: packages/features/data-table/components/DataTable.tsx
user: login: sean-brydon
body: Yep as per designs
created_at: 2025-05-07T09:31:57Z
author_association: MEMBER

PR comments:
user: login: graphite-app[bot]
created_at: 2025-05-07T09:35:11Z
body: ### Graphite Automations
<details><summary><a href="https://app.graphite.dev/automations/?org=calcom&ruleId=tJdOAwF9biWptZ2BLCTk&ref=gt-automation-github-comment">"Add ready-for-e2e label"</a> took an action on this PR • (05/07/25)</summary><p>1 label was added to this PR based on Keith Williams's automation.</p></details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-07T09:58:49Z
body: <!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #59229.1 latest results](https://calcom.github.io/test-results/reports/feat/v3-table-designs/14880048729/1)
author_association: CONTRIBUTOR