diff --git a/apps/client/src/features/base/components/base-table-skeleton.tsx b/apps/client/src/features/base/components/base-table-skeleton.tsx
new file mode 100644
index 00000000..dfcae12f
--- /dev/null
+++ b/apps/client/src/features/base/components/base-table-skeleton.tsx
@@ -0,0 +1,84 @@
+import { Skeleton } from "@mantine/core";
+import gridClasses from "@/features/base/styles/grid.module.css";
+import classes from "@/features/base/styles/base-table-skeleton.module.css";
+
+const ROW_NUMBER_WIDTH = 64;
+const COLUMN_WIDTH = 180;
+const COLUMN_COUNT = 6;
+const ROW_COUNT = 10;
+
+// Deterministic per-cell widths so the skeleton doesn't flicker between
+// renders. Values are rough normal distribution around 55-85 % of cell.
+const CELL_WIDTH_RATIOS = [0.78, 0.62, 0.84, 0.55, 0.71, 0.66];
+const HEADER_WIDTH_RATIOS = [0.42, 0.58, 0.5, 0.64, 0.46, 0.54];
+
+export function BaseTableSkeleton() {
+ const gridTemplateColumns = [
+ `${ROW_NUMBER_WIDTH}px`,
+ ...Array.from({ length: COLUMN_COUNT }, () => `${COLUMN_WIDTH}px`),
+ ].join(" ");
+
+ return (
+
+
+
+
+
+
+ {Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
+
+ ))}
+
+ {Array.from({ length: ROW_COUNT }).map((_, rowIndex) => (
+
+
+ {Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
+
+ ))}
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/client/src/features/base/styles/base-table-skeleton.module.css b/apps/client/src/features/base/styles/base-table-skeleton.module.css
new file mode 100644
index 00000000..d610f867
--- /dev/null
+++ b/apps/client/src/features/base/styles/base-table-skeleton.module.css
@@ -0,0 +1,47 @@
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: var(--mantine-spacing-xs);
+ padding: var(--mantine-spacing-xs) 0;
+ margin-bottom: var(--mantine-spacing-xs);
+}
+
+.toolbarTabs {
+ display: flex;
+ gap: 6px;
+ flex: 1;
+}
+
+.toolbarActions {
+ display: flex;
+ gap: var(--mantine-spacing-xs);
+ margin-left: auto;
+}
+
+.gridWrapper {
+ overflow: hidden;
+ flex: 1;
+ border-top: 1px solid
+ light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
+}
+
+.grid {
+ display: grid;
+}
+
+.cellInner {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ width: 100%;
+ padding: 0 8px;
+}
+
+.headerCellInner {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ height: 100%;
+ width: 100%;
+ padding: 0 8px;
+}