27

How to make a table with so little JavaScript as possible using modern CSS?

The features I'm trying to have is:

  • fixed column(s) (positioning and width)
  • scrollable in X and Y axis
  • responsive in the X axis (for non fixed width columns).

enter image description here

Note: I did see and analyse some of the most popular/seen questions and answers in SO, like here and here for example.

I know this can be done with JavaScript event handlers for scroll so that a fixed column can be moved down/up to follow the main column. A example of the functionality I am trying to build (but heavily scripted) could be like this. We can also add MutationObserver in the parent element to the table to detect size changes and calculate again the sizes of the table. But this is expensive in performance, and since CSS is also evolving and modernizing I wonder if there are new ways...

Is there a 2017/2018 solution for this that is only CSS or as little JS overhead as possible?

I would like to avoid having to implement JavaScript solutions that break so easy like this:

enter image description here

My ideas:

a): use position: fixed; in the fixed columns

Problems:

  • <td> elements height has to be defined or calculated by JavaScript.
  • a scroll event listener is needed and we need to scroll the fixed column programmatically

b): use div(s) to "fake" the left column and have the scrollable content in the table

Problems:

  • div height has to sync with table rows height
  • HTML semantics lost since fixed "fake" column is not table syntax any more

c): use N tables show only parts of each so I can have HTML markup but keep fixed header/columns.

Problems:

  • repeated HTML x N
  • have to sync sizes and scroll with JavaScript also

Using a scroll event listener and a fixed left column we can use JavaScript like this:

const firstColumn = [...document.querySelectorAll('table tr > *:first-of-type')];
const tableContainer = document.querySelector('.table-container');
tableContainer.addEventListener('scroll', function() {
    const currentScrollPosition = this.scrollTop * -1;
 firstColumn.forEach(el => el.style.marginTop = currentScrollPosition + 'px');
});
.table-container {
    width: 600px;
    height: 300px;
    overflow-x: scroll;
    overflow-y: scroll;
}

.table-scroller {
    width: 1000px;
}

table {
    width: 100%;
    margin-left: -13px;
    table-layout: fixed;
    border-collapse: collapse;
    border: none;
}

table th,
table td {
    padding: 0.8em;
    border: 1px solid;
    background-color: #eeeeef;
}

table th {
    background-color: #6699FF;
    font-weight: bold;
}

table tr {
    height: 60px;
    vertical-align: middle;
}

table tr > *:first-of-type {
    position: fixed;
    width: 50px;
    margin-left: 13px;
    margin-top: 0;
    height: inherit;
}
<div class="table-container">
    <div class="table-scroller">
        <table>
            <tbody>
                <tr>
                    <td>Edrward 0</td>
                    <td>32</td>
                    <td>London Park no. 0</td>
                    <td>London Park no. 0</td>
                    <td>London Park no. 0</td>
                    <td>London Park no. 0</td>
                    <td>London Park no. 0</td>
                    <td>London Park no. 0</td>
                    <td>London Park no. 0</td>
                    <td>London Park no. 0</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 1</td>
                    <td>32</td>
                    <td>London Park no. 1</td>
                    <td>London Park no. 1</td>
                    <td>London Park no. 1</td>
                    <td>London Park no. 1</td>
                    <td>London Park no. 1</td>
                    <td>London Park no. 1</td>
                    <td>London Park no. 1</td>
                    <td>London Park no. 1</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 2</td>
                    <td>32</td>
                    <td>London Park no. 2</td>
                    <td>London Park no. 2</td>
                    <td>London Park no. 2</td>
                    <td>London Park no. 2</td>
                    <td>London Park no. 2</td>
                    <td>London Park no. 2</td>
                    <td>London Park no. 2</td>
                    <td>London Park no. 2</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 3</td>
                    <td>32</td>
                    <td>London Park no. 3</td>
                    <td>London Park no. 3</td>
                    <td>London Park no. 3</td>
                    <td>London Park no. 3</td>
                    <td>London Park no. 3</td>
                    <td>London Park no. 3</td>
                    <td>London Park no. 3</td>
                    <td>London Park no. 3</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 4</td>
                    <td>32</td>
                    <td>London Park no. 4</td>
                    <td>London Park no. 4</td>
                    <td>London Park no. 4</td>
                    <td>London Park no. 4</td>
                    <td>London Park no. 4</td>
                    <td>London Park no. 4</td>
                    <td>London Park no. 4</td>
                    <td>London Park no. 4</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 5</td>
                    <td>32</td>
                    <td>London Park no. 5</td>
                    <td>London Park no. 5</td>
                    <td>London Park no. 5</td>
                    <td>London Park no. 5</td>
                    <td>London Park no. 5</td>
                    <td>London Park no. 5</td>
                    <td>London Park no. 5</td>
                    <td>London Park no. 5</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 6</td>
                    <td>32</td>
                    <td>London Park no. 6</td>
                    <td>London Park no. 6</td>
                    <td>London Park no. 6</td>
                    <td>London Park no. 6</td>
                    <td>London Park no. 6</td>
                    <td>London Park no. 6</td>
                    <td>London Park no. 6</td>
                    <td>London Park no. 6</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 7</td>
                    <td>32</td>
                    <td>London Park no. 7</td>
                    <td>London Park no. 7</td>
                    <td>London Park no. 7</td>
                    <td>London Park no. 7</td>
                    <td>London Park no. 7</td>
                    <td>London Park no. 7</td>
                    <td>London Park no. 7</td>
                    <td>London Park no. 7</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 8</td>
                    <td>32</td>
                    <td>London Park no. 8</td>
                    <td>London Park no. 8</td>
                    <td>London Park no. 8</td>
                    <td>London Park no. 8</td>
                    <td>London Park no. 8</td>
                    <td>London Park no. 8</td>
                    <td>London Park no. 8</td>
                    <td>London Park no. 8</td>
                    <td><a href="#">action</a></td>
                </tr>
                <tr>
                    <td>Edrward 9</td>
                    <td>32</td>
                    <td>London Park no. 9</td>
                    <td>London Park no. 9</td>
                    <td>London Park no. 9</td>
                    <td>London Park no. 9</td>
                    <td>London Park no. 9</td>
                    <td>London Park no. 9</td>
                    <td>London Park no. 9</td>
                    <td>London Park no. 9</td>
                    <td><a href="#">action</a></td>
                </tr>
            </tbody>
        </table>
    </div>
</div>

But can I do this without JavaScript?


About the possible duplicate suggestion:

That possible duplicate is mentioned in my question and it does not have what I'm looking for. That question asks for "just a single left column to be frozen" - the scope of this one is more broad, its relates to fixed header, fixed columns and scrollable body.
The other question and the accepted answer are from 2009! I think this topic, with my specific scope and examples is worth being revisited. Also: my "JS solution" is just a example how JS can break, I'm looking for modern CSS techniques for doing just that.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Rikard
  • 7,485
  • 11
  • 55
  • 92
  • You want only one table, or nested elements are acceptable? – klenium Sep 18 '17 at 11:06
  • @klenium I have no wishes there, the idea is to have CSS doing the scrolling, resizing, responsiveness. Maybe we are not there yet with CSS and we might need JavaScript. If you have a idea please share it. The questions and links I named are some single table, some multiple. But they have limitations and overhead that maybe in 2018 we do not need anymore (?). – Rikard Sep 18 '17 at 11:14
  • Your JS "solution" isn't perfect and breaks when the main page is scrolled – Zach Saucier Sep 19 '17 at 03:17
  • Possible duplicate of [how do I create an HTML table with fixed/frozen left column and scrollable body?](https://stackoverflow.com/questions/1312236/how-do-i-create-an-html-table-with-fixed-frozen-left-column-and-scrollable-body) – Zach Saucier Sep 19 '17 at 03:17
  • 2
    @ZachSaucier that possible duplicate is mentioned in my question and it does not have what I'm looking for. That question asks for _"just a single left column to be frozen"_ - the scope of this one is more broad, its relates to fixed header, fixed column**s** and scrollable body. The other question and the accepted answer are from 2009! I think this topic, with my specific scope and examples is worth being revisited. Also: my "JS solution" is just a example how JS can break, I'm looking for modern CSS techniques for doing just that. – Rikard Sep 19 '17 at 05:46
  • There is a very nice example here https://codepen.io/Kamilius/pen/LpObwW – user3808307 Aug 27 '20 at 14:42

2 Answers2

39

Maybe you could try to use the recent position:sticky to achieve it. Or, at least, to start the aproach with less javascript.

div{
  overflow:auto;
  width:100%;
  height:200px;
}
td,
th {
  border: 1px solid #000;
  width: 100px;
}
th {background-color:red;}

table {
  table-layout: fixed;
  width:100%;
}
td:first-child, th:first-child {
  position:sticky;
  left:0;
  z-index:1;
  background-color:grey;
}
td:last-child, th:last-child {
  position:sticky;
  right:0;
  z-index:1;
  background-color:blue;
}
thead tr th {
  position:sticky;
  top:0;
}
th:first-child, th:last-child {z-index:2;background-color:red;}
<div>
  <table>
    <thead>
      <tr>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
        <th>&nbsp;&nbsp;&nbsp;</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
      <tr>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
        <td>&nbsp;&nbsp;&nbsp;</td>
      </tr>
    </tbody>
  </table>
</div>
Alvaro Menéndez
  • 8,766
  • 3
  • 37
  • 57
  • 1
    fixed height would be a no go, it should 100% height of whatever container the table is in – PirateApp Sep 22 '18 at 06:05
  • 2
    The fixed height is added to show the sticky header in the example, to make it as close as possible as the gif provided... – Alvaro Menéndez Sep 22 '18 at 12:46
  • 1
    For those who aren't aware (like I wasn't), you can change `table-layout: fixed` to `table-layout: auto` to allow your column widths to resize automatically. This may slow down rendering a little big on very large tables because the browser needs to wait for the whole table to load before it can determine the correct column sizes. –  Sep 27 '18 at 05:45
  • 2
    Brilliant. Very compact and readable code backing a robust example. – Cincy Steve Sep 18 '20 at 17:52
  • Here's an Codepen Pen if anyone interests. https://codepen.io/kavinda1995/pen/OJbXxGw – Kavinda Jayakody Feb 08 '21 at 21:01
0

Just look at my code with a fixed header and fix the first column.

    <style type="text/css">
    .table {
          font-family: arial, sans-serif;
  border-collapse: collapse;
}

.th {
  width: 126px;
  height: 56px;
  padding: 0px 16px;
  text-align: left;
  background-color: #fafafa;
}

.td {
  width: 118px;
  min-width: 118px;
  max-width: 118px;
  height: 56px;
  max-height: 56px;
  overflow: hidden;
  text-align: center;
  border-bottom: 2px solid #dddddd;
}
.pink_color {
  background-color: pink;
}

.table_wrapper {
  position: relative;
}
.table_scroll {
  height: 73vh;
  overflow: auto;
}
.table_wrapper table {
  width: 100%;
}

.table_wrapper table thead th .text {
  position: absolute;
  z-index: 2;
}
.tableFixHead
{ 
  overflow: auto; height: 63vh; 
}
.tableFixHead thead th 
{ 
  position: sticky; top: 0; z-index: 1; 
}


@media only screen and (min-width: 1440px) {
  .table_scroll {
    height: 77vh;
    overflow: auto;
  }
}
</style>

<div style="padding-top: 16px;">
    <div class="table_wrapper">
        <div class="tableFixHead">
            <table class="table">
                <thead>
                  <th style="position: sticky; left: 0; z-index: 3;" class="text th"></th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                  <th class="text th">1</th>
                </thead>
                <tr>
                  <td style="position: sticky; left: 0; z-index: 2; background: #fafafa;" class="td">test</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                </tr>
                <tr>
                  <td style="position: sticky; left: 0; z-index: 2; background: #fafafa;" class="td">test</td>
                  <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                </tr>
                <tr>
                  <td style="position: sticky; left: 0; z-index: 2; background: #fafafa;" class="td">test</td>
                  <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                </tr><tr>
                  <td style="position: sticky; left: 0; z-index: 2; background: #fafafa;" class="td">test</td>
                  <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                </tr><tr>
                  <td style="position: sticky; left: 0; z-index: 2; background: #fafafa;" class="td">test</td>
                  <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                </tr><tr>
                  <td style="position: sticky; left: 0; z-index: 2; background: #fafafa;" class="td">test</td>
                  <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                </tr><tr>
                  <td style="position: sticky; left: 0; z-index: 2; background: #fafafa;" class="td">test</td>
                  <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                    <td class="td">test1</td>
                </tr>
            </table>
        </div>
    </div>
</div>