28

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.

6
  • You want only one table, or nested elements are acceptable? Commented Sep 18, 2017 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 (?). Commented Sep 18, 2017 at 11:14
  • Your JS "solution" isn't perfect and breaks when the main page is scrolled Commented Sep 19, 2017 at 3:17
  • Possible duplicate of how do I create an HTML table with fixed/frozen left column and scrollable body? Commented Sep 19, 2017 at 3: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 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. Commented Sep 19, 2017 at 5:46

2 Answers 2

43

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>

Sign up to request clarification or add additional context in comments.

5 Comments

fixed height would be a no go, it should 100% height of whatever container the table is in
The fixed height is added to show the sticky header in the example, to make it as close as possible as the gif provided...
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.
Brilliant. Very compact and readable code backing a robust example.
Here's an Codepen Pen if anyone interests. codepen.io/kavinda1995/pen/OJbXxGw
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>

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.