From 137c02a10ff4e531cdff2bf1b09d9fd5b650a85a Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sat, 25 Apr 2026 01:05:20 +0100 Subject: [PATCH] feat(base): sticky table header so columns stay visible while scrolling Pins the header row to the top of the gridWrapper scroll viewport, with the pinned-left corner cell stacking above pinned body cells at the top-left intersection. The trailing add-column button gets the same sticky treatment so it doesn't drift away from the row. Subtle bit: `position: sticky` confines an element to its containing block. The previous subgrid `.headerRow` wrapper was 34px tall, so sticky cells could only travel 34px before scrolling out with the wrapper. Switching the wrapper to `display: contents` lets the cells become direct grid children of `.grid` (full table height) so sticky travel matches the scroll range. role="row" survives display:contents in modern browsers. --- .../src/features/base/styles/grid.module.css | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/client/src/features/base/styles/grid.module.css b/apps/client/src/features/base/styles/grid.module.css index 0bd211509..2dccc4dec 100644 --- a/apps/client/src/features/base/styles/grid.module.css +++ b/apps/client/src/features/base/styles/grid.module.css @@ -16,13 +16,24 @@ } .headerRow { - display: grid; - grid-column: 1 / -1; - grid-template-columns: subgrid; + /* `display: contents` removes the wrapper from layout while keeping the + * `role="row"` for accessibility. Header cells become direct grid items + * of `.grid`, so their containing block is the full-height table — a + * prerequisite for `position: sticky` on `.headerCell` to actually + * travel the length of the scroll. With a subgrid wrapper here, sticky + * was constrained to the 34px header row and scrolled away. */ + display: contents; } .headerCell { - position: relative; + /* Sticky to the top of the gridWrapper scroll viewport so the column + * header row stays visible while the user scrolls rows underneath. + * z-index 2 lifts it above non-pinned body cells (default 0); the + * pinned variant below bumps to 3 so the corner cell stays above + * pinned body cells (z-index 1) at the top-left intersection. */ + position: sticky; + top: 0; + z-index: 2; display: flex; align-items: center; gap: 6px; @@ -51,8 +62,11 @@ } .headerCellPinned { - position: sticky; - z-index: 2; + /* Sticks both vertically (inherited top: 0) and horizontally (left + * offset set inline by tanstack-table's column-pinning), so the row- + * number / primary-property column stays visible at the top-left + * corner regardless of scroll axis. */ + z-index: 3; background-color: light-dark( var(--mantine-color-gray-0), var(--mantine-color-dark-6) @@ -236,6 +250,12 @@ } .addColumnButton { + /* Sits at the trailing edge of the header row — match the sticky-top + * behaviour of `.headerCell` so it doesn't drift away when the grid + * scrolls vertically. */ + position: sticky; + top: 0; + z-index: 2; display: flex; align-items: center; justify-content: center;