title: Scrollable table with fixed columns and header, with modern CSS
body: <p>How to make a table with so little JavaScript as possible using modern CSS?</p>
  
  <p>The features I'm trying to have is:</p>
  
  <ul>
  <li>fixed column(s) (positioning and width)</li>
  <li>scrollable in X and Y axis</li>
  <li>responsive in the X axis (for non fixed width columns).</li>
  </ul>
  
  <p><a href="https://i.stack.imgur.com/tVi2v.gif" rel="noreferrer"><img src="https://i.stack.imgur.com/tVi2v.gif" alt="enter image description here"></a></p>
  
  <p><strong>Note:</strong> I did see and analyse some of the most popular/seen questions and answers in SO, like <a href="https://stackoverflow.com/q/1312236/2427193">here</a> and <a href="https://stackoverflow.com/q/21168521/2427193">here</a> for example.</p>
  
  <p>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) <a href="https://codepen.io/anon/pen/veNPgq" rel="noreferrer">could be like this</a>. We can also add <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver" rel="noreferrer"><code>MutationObserver</code></a> 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...</p>
  
  <p>Is there a 2017/2018 solution for this that is only CSS or as little JS overhead as possible?</p>
  
  <p>I would like to avoid having to implement JavaScript solutions that break so easy like this:</p>
  
  <p><a href="https://i.stack.imgur.com/db9Mg.png" rel="noreferrer"><img src="https://i.stack.imgur.com/db9Mg.png" alt="enter image description here"></a></p>
  
  <p>My ideas:</p>
  
  <p><strong><code>a)</code></strong>: use <code>position: fixed;</code> in the fixed columns</p>
  
  <p>Problems: </p>
  
  <ul>
  <li><code>&lt;td&gt;</code> elements height has to be defined or calculated by JavaScript.</li>
  <li>a scroll event listener is needed and we need to scroll the fixed column programmatically</li>
  </ul>
  
  <p><strong><code>b)</code></strong>: use div(s) to "fake" the left column and have the scrollable content in the table</p>
  
  <p>Problems:</p>
  
  <ul>
  <li>div height has to sync with table rows height</li>
  <li>HTML semantics lost since fixed "fake" column is not table syntax any more</li>
  </ul>
  
  <p><strong><code>c)</code></strong>: use <em>N</em> tables show only parts of each so I can have HTML markup but keep fixed header/columns. </p>
  
  <p>Problems:</p>
  
  <ul>
  <li>repeated HTML x <em>N</em></li>
  <li>have to sync sizes and scroll with JavaScript also</li>
  </ul>
  
  <hr>
  
  <p>Using a scroll event listener and a fixed left column we can use JavaScript like this: </p>
  
  <p><div class="snippet" data-lang="js" data-hide="true" data-console="true" data-babel="false">
  <div class="snippet-code snippet-currently-hidden">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>const firstColumn = [...document.querySelectorAll('table tr &gt; *:first-of-type')];
  const tableContainer = document.querySelector('.table-container');
  tableContainer.addEventListener('scroll', function() {
      const currentScrollPosition = this.scrollTop * -1;
   firstColumn.forEach(el =&gt; el.style.marginTop = currentScrollPosition + 'px');
  });</code></pre>
  <pre class="snippet-code-css lang-css prettyprint-override"><code>.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 &gt; *:first-of-type {
      position: fixed;
      width: 50px;
      margin-left: 13px;
      margin-top: 0;
      height: inherit;
  }</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;div class="table-container"&gt;
      &lt;div class="table-scroller"&gt;
          &lt;table&gt;
              &lt;tbody&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 0&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 0&lt;/td&gt;
                      &lt;td&gt;London Park no. 0&lt;/td&gt;
                      &lt;td&gt;London Park no. 0&lt;/td&gt;
                      &lt;td&gt;London Park no. 0&lt;/td&gt;
                      &lt;td&gt;London Park no. 0&lt;/td&gt;
                      &lt;td&gt;London Park no. 0&lt;/td&gt;
                      &lt;td&gt;London Park no. 0&lt;/td&gt;
                      &lt;td&gt;London Park no. 0&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 1&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 1&lt;/td&gt;
                      &lt;td&gt;London Park no. 1&lt;/td&gt;
                      &lt;td&gt;London Park no. 1&lt;/td&gt;
                      &lt;td&gt;London Park no. 1&lt;/td&gt;
                      &lt;td&gt;London Park no. 1&lt;/td&gt;
                      &lt;td&gt;London Park no. 1&lt;/td&gt;
                      &lt;td&gt;London Park no. 1&lt;/td&gt;
                      &lt;td&gt;London Park no. 1&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 2&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 2&lt;/td&gt;
                      &lt;td&gt;London Park no. 2&lt;/td&gt;
                      &lt;td&gt;London Park no. 2&lt;/td&gt;
                      &lt;td&gt;London Park no. 2&lt;/td&gt;
                      &lt;td&gt;London Park no. 2&lt;/td&gt;
                      &lt;td&gt;London Park no. 2&lt;/td&gt;
                      &lt;td&gt;London Park no. 2&lt;/td&gt;
                      &lt;td&gt;London Park no. 2&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 3&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 3&lt;/td&gt;
                      &lt;td&gt;London Park no. 3&lt;/td&gt;
                      &lt;td&gt;London Park no. 3&lt;/td&gt;
                      &lt;td&gt;London Park no. 3&lt;/td&gt;
                      &lt;td&gt;London Park no. 3&lt;/td&gt;
                      &lt;td&gt;London Park no. 3&lt;/td&gt;
                      &lt;td&gt;London Park no. 3&lt;/td&gt;
                      &lt;td&gt;London Park no. 3&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 4&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 4&lt;/td&gt;
                      &lt;td&gt;London Park no. 4&lt;/td&gt;
                      &lt;td&gt;London Park no. 4&lt;/td&gt;
                      &lt;td&gt;London Park no. 4&lt;/td&gt;
                      &lt;td&gt;London Park no. 4&lt;/td&gt;
                      &lt;td&gt;London Park no. 4&lt;/td&gt;
                      &lt;td&gt;London Park no. 4&lt;/td&gt;
                      &lt;td&gt;London Park no. 4&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 5&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 5&lt;/td&gt;
                      &lt;td&gt;London Park no. 5&lt;/td&gt;
                      &lt;td&gt;London Park no. 5&lt;/td&gt;
                      &lt;td&gt;London Park no. 5&lt;/td&gt;
                      &lt;td&gt;London Park no. 5&lt;/td&gt;
                      &lt;td&gt;London Park no. 5&lt;/td&gt;
                      &lt;td&gt;London Park no. 5&lt;/td&gt;
                      &lt;td&gt;London Park no. 5&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 6&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 6&lt;/td&gt;
                      &lt;td&gt;London Park no. 6&lt;/td&gt;
                      &lt;td&gt;London Park no. 6&lt;/td&gt;
                      &lt;td&gt;London Park no. 6&lt;/td&gt;
                      &lt;td&gt;London Park no. 6&lt;/td&gt;
                      &lt;td&gt;London Park no. 6&lt;/td&gt;
                      &lt;td&gt;London Park no. 6&lt;/td&gt;
                      &lt;td&gt;London Park no. 6&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 7&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 7&lt;/td&gt;
                      &lt;td&gt;London Park no. 7&lt;/td&gt;
                      &lt;td&gt;London Park no. 7&lt;/td&gt;
                      &lt;td&gt;London Park no. 7&lt;/td&gt;
                      &lt;td&gt;London Park no. 7&lt;/td&gt;
                      &lt;td&gt;London Park no. 7&lt;/td&gt;
                      &lt;td&gt;London Park no. 7&lt;/td&gt;
                      &lt;td&gt;London Park no. 7&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 8&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 8&lt;/td&gt;
                      &lt;td&gt;London Park no. 8&lt;/td&gt;
                      &lt;td&gt;London Park no. 8&lt;/td&gt;
                      &lt;td&gt;London Park no. 8&lt;/td&gt;
                      &lt;td&gt;London Park no. 8&lt;/td&gt;
                      &lt;td&gt;London Park no. 8&lt;/td&gt;
                      &lt;td&gt;London Park no. 8&lt;/td&gt;
                      &lt;td&gt;London Park no. 8&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
                  &lt;tr&gt;
                      &lt;td&gt;Edrward 9&lt;/td&gt;
                      &lt;td&gt;32&lt;/td&gt;
                      &lt;td&gt;London Park no. 9&lt;/td&gt;
                      &lt;td&gt;London Park no. 9&lt;/td&gt;
                      &lt;td&gt;London Park no. 9&lt;/td&gt;
                      &lt;td&gt;London Park no. 9&lt;/td&gt;
                      &lt;td&gt;London Park no. 9&lt;/td&gt;
                      &lt;td&gt;London Park no. 9&lt;/td&gt;
                      &lt;td&gt;London Park no. 9&lt;/td&gt;
                      &lt;td&gt;London Park no. 9&lt;/td&gt;
                      &lt;td&gt;&lt;a href="#"&gt;action&lt;/a&gt;&lt;/td&gt;
                  &lt;/tr&gt;
              &lt;/tbody&gt;
          &lt;/table&gt;
      &lt;/div&gt;
  &lt;/div&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <p>But can I do this without JavaScript?</p>
  
  <hr>
  
  <p><strong>About the possible duplicate suggestion:</strong></p>
  
  <p>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.<br>
  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. </p>