'Grid with fixed column and row (freeze panes) using CSS Grid (and possibly Mask/Clip/Javascript - NOT HTML tables)

I have a need to create a grid of data. Ideally, I would like to use CSS Grid. The grid will be potentially large and need to scroll horizontally and vertically. However, the first row and the first column must always be visible. The effect I'm going for is similar to "freezing panes" in Excel (sometimes called "locking" rows and/or columns).

Additionally, after scrolling, I'll need to be able to click on items in the grid as well as implement some drag/drop behavior.

I have this working using a technique that utilizes HTML tables and the sticky and z-index CSS properties. You can see that working in the following fiddle: https://jsfiddle.net/dmboucher/txcLkq60/26/

Notice how you can scroll horizontally and vertically, but you can always see the first row and the first column.

There are reasons why using html tables for this is less than ideal. I would rather use CSS Grid or FlexBox... no html tables. My experiments with CSS Grid and FlexBox have failed so far.

I investigated the concept of using CSS masking and/or clipping. The idea here would be to have a massive div (the grid) in the background, but only be able to view it "through" a mask "window", then use scrollbars and javascript to move the large div behind the mask such that you can see the desired portion of the grid. Something to that effect. I have not been able to get that to work either.

The only method that has been successful has been with html tables.

Can this be done without using html tables? Other suggestions?

Thank you for your help!



Solution 1:[1]

I eventually figured out a way to do this. I laid out a FlexBox grid of div's with the appropriate stylings. Conceptually, I laid out a 9 "cell" matrix like so:

. . .
Upper left Times Spacer
Employees Schedule Vertical Scrollbar
Spacer Horizontal Scrollbar Spacer

In this way, Times will scroll horizontally, Employees will scroll vertically, and Schedule will scroll both horizontally and vertically.

The key was that Employees, Schedule, and Vertical Scrollbar all had to have the same computed height. Similarly, Times, Schedule, and Horizontal Scrollbar all had to have the same computed width. If not, then the scrolling would get janky.

Once all the styles were added such that everything rendered properly (i.e. overflow, flex-shrink, etc.), then, I added onscroll functions to the Horizontal and Vertical Scrollbar elements. Whenever horizontal scroll events fire, I synchronize the schedule and times scrollLeft values. Whenever vertical scroll events fire, I synchronize the schedule and times scrollTop values.

The relevant sync functions are like so:

function bodyOnLoad() { 
    let id = "timeline";
    let tag = `<div class="timeline_grid_row timeline_grid_header_row">
        <div class="timeline_grid_controls">Upper Left</div>
        <div id="${id}_timeline_calendar_container_grid_times" class="timeline_grid_times">
            ${[...Array(24).keys()].map((i) => {
                return `<div class="timeline_grid_cell">hour ${i}</div>`;
            }).join("\n")}
        </div>
        <div class="timeline_grid_spacer_cell ur">&nbsp;</div>
    </div>
    <div class="timeline_grid_row timeline_grid_body_row">
        <div id="${id}_timeline_calendar_container_grid_employees" class="timeline_grid_employees">
            ${[...Array(35).keys()].map((i) => {
                return `<div class="timeline_grid_employee_cell">worker ${i}</div>`;
            }).join("\n")}
        </div>
        <div id="${id}_timeline_calendar_container_grid_schedule" class="timeline_grid_schedule">
            ${[...Array(35).keys()].map((i) => {
                return `<div class="timeline_grid_schedule_row">
                    ${[...Array(24).keys()].map((j) => {
                        return `<div class="timeline_grid_cell">content ${i}-${j}</div>`;
                    }).join("\n")}
                </div>`;
            }).join("\n")}
        </div>
        <div class="timeline_grid_scroll_vertical" onscroll="TimelineSynchronizeVerticalScroll('${id}', this)">
            ${[...Array(35).keys()].map((i) => {
                return '<div class="timeline_grid_spacer_cell cr">&nbsp;</div>';;
            }).join("\n")}
        </div>
    </div>
    <div class="timeline_grid_row timeline_grid_footer_row">
        <div class="timeline_grid_spacer_cell ll">&nbsp;</div>
        <div class="timeline_grid_scroll_horizontal" onscroll="TimelineSynchronizeHorizontalScroll('${id}', this)">
            ${[...Array(24).keys()].map((i) => {
                return '<div class="timeline_grid_horizontal_filler_cell">&nbsp;</div>';
            }).join("\n")}
        </div>
        <div class="timeline_grid_spacer_cell lr">&nbsp;</div>
    </div>`;
    document.getElementById(`${id}_calendar_container_grid`).innerHTML = tag;  // write to DOM
}

function TimelineSynchronizeHorizontalScroll(id, scrollControl) {
    document.getElementById(`${id}_timeline_calendar_container_grid_schedule`).scrollLeft = scrollControl.scrollLeft;
    document.getElementById(`${id}_timeline_calendar_container_grid_times`).scrollLeft    = scrollControl.scrollLeft;
}

function TimelineSynchronizeVerticalScroll(id, scrollControl) {
    document.getElementById(`${id}_timeline_calendar_container_grid_schedule`).scrollTop  = scrollControl.scrollTop;
    document.getElementById(`${id}_timeline_calendar_container_grid_employees`).scrollTop = scrollControl.scrollTop;
}
.timeline_control .timeline_calendar {
  display: flex;
  width: 500px;
  height: 150px;
  flex-shrink: 0;
}
.timeline_control .timeline_calendar .timeline_calendar_container_grid * {
    display: flex;
}
.timeline_control .timeline_calendar .timeline_calendar_container_grid {
    display: flex;
    flex-direction: column;
    overflow: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_row {
    overflow: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_row.timeline_grid_header_row,
.timeline_control .timeline_calendar .timeline_grid_row.timeline_grid_footer_row {
    flex-shrink: 0;
}
.timeline_control .timeline_calendar .timeline_grid_controls {
    flex-shrink: 0;
    width: 150px;
}
.timeline_control .timeline_calendar .timeline_grid_times {
    overflow: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_employees {
    flex-direction: column;
    flex-shrink: 0;
    overflow: hidden;
    width: 150px;
}
.timeline_control .timeline_calendar .timeline_grid_schedule {
    flex-direction: column;
    overflow: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_cell {
    flex-shrink: 0;
    margin: 0 10px;
    width: 100px;
}
.timeline_control .timeline_calendar .timeline_grid_scroll_horizontal {
    height: 18px;
    overflow-x: auto;
    overflow-y: hidden;
}
.timeline_control .timeline_calendar .timeline_grid_scroll_vertical {
    flex-direction: column;
    flex-shrink: 0;
    overflow-x: hidden;
    overflow-y: auto;
    width: 18px;
}
.timeline_control .timeline_calendar .timeline_grid_horizontal_filler_cell {
    flex-shrink: 0;
    margin: 0 10px;
    width: 100px;
}
.timeline_control .timeline_calendar .timeline_grid_spacer_cell {
    flex-shrink: 0;
}
.timeline_control .timeline_calendar .timeline_grid_spacer_cell.ur {
    width: 8px;
}
.timeline_control .timeline_calendar .timeline_grid_spacer_cell.cr,
.timeline_control .timeline_calendar .timeline_grid_spacer_cell.lr {
    width: 18px;
}
.timeline_control .timeline_calendar .timeline_grid_spacer_cell.ll {
    width: 150px;
}
<body onload="bodyOnLoad()">
  <div class="timeline_control">
    <div class="timeline_calendar">
      <div id="timeline_calendar_container_grid" class="timeline_calendar_container_grid"></div>
    </div>
  </div>
</body>

Now I have the UI I'm going for... with no html tables. Mission Accomplished!

I just thought I'd share this solution in case it helps others in the future.

Here is a working fiddle of this solution: https://jsfiddle.net/dmboucher/Lwqdcpv8/

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 dmboucher