skip to Main Content

I am using HTML, CSS, and JS to create a page to display my CSV data neatly. The table being used to display the CSV data is inside a flexbox container div. Currently, the max height of the container is set to 95vh, and once the table starts the exceeds the height, it makes the entire div container scrollable.

However, I am only looking to make the table data itself scrollable (i.e. <tbody>), not the entire container. This effectively means I want to keep my <h2> header and <thead> header, inside the <div> container, stationery while making the table scrollable if it exceeds the max height of the container.

I have looked at several past SO posts to make only the table content scrollable, and attempted CSS methods such as display: block and position: sticky, but it does not achieve the desired effect I intend for.

If there is a duplicate post to mine (that exactly solves my issue), please link me to it, as I have extensively searched for duplicates. And I would like to solve this issue using pure HTML/CSS/JS or with the help of external scripts.

MY CODE (you can also tinker with my JSFiddle)

// Define CSV data here
const testData = `Value1A,Value1B,Value1C
Value2A,Value2B,Value2C
Value3A,Value3B,Value3C
Value4A,Value4B,Value4C
Value5A,Value5B,Value5C
Value6A,Value6B,Value6C
Value7A,Value7B,Value7C
Value8A,Value8B,Value8C
Value9A,Value9B,Value9C
Value10A,Value10B,Value10C
Value11A,Value11B,Value11C
Value12A,Value12B,Value12C
Value13A,Value13B,Value13C
Value14A,Value14B,Value14C
Value15A,Value15B,Value15C`;

// Split the CSV data
let rows = testData.split("n");

// Add table tags to each row and cell
let table = "";
rows.forEach(function(row) {
  const columns = row.split(/,(?=(?:[^"]*"[^"]*")*(?![^"]*"))/);
  table += `<tr>`;

  // Iterate over each column
  columns.forEach(function(column) {
    table += "<td>" + column.replace(/^"(.*)"$/, "$1") + "</td>";
  });

  table += "</tr>";
});

// Update the table body
const tbody = document.querySelector("#csv-table tbody");
tbody.innerHTML = table;
* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 0;
  font-family: Arial, sans-serif;
  background-color: #F3F2F1;
  background-color: #d9efa1;
}

.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  max-width: 1024px;
  margin: 0 auto;
  padding: 5%;
}

.csv {
  background-color: #FFFFFF;
  border-radius: 10px;
  padding: 3%;
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
  max-height: 95vh;
  overflow: auto;
}

.csv h2 {
  margin-top: 1%;
  margin-bottom: 3%;
  margin-left: 0.1%;
  color: #f6b40e;
}

.csv table {
  width: 100%;
  border-collapse: collapse;
  table-layout: fixed;
}

.csv table thead th {
  background-color: #f6b40e;
  color: #FFFFFF;
  padding: 3%;
  text-align: left;
}

.csv table tbody tr:nth-child(even) {
  background-color: #F3F2F1;
}

.csv table tbody td {
  padding: 3%;
  word-wrap: break-word;
}
<!DOCTYPE html>
<html>

<head>
  <title>CSV</title>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>

<body>
  <div class="container">
    <div class="csv">
      <h2>CSV</h2>
      <table id="csv-table">
        <thead>
          <tr>
            <th>A</th>
            <th>B</th>
            <th>C</th>
          </tr>
        </thead>
        <tbody>
          <!-- Dynamically generated rows will be added here -->
        </tbody>
      </table>
    </div>
  </div>
</body>

</html>

3

Answers


  1. We add selectors for tbody, thead tr and th, td and use display: block.

    // Define CSV data here
    const testData = `Value1A,Value1B,Value1C
    Value2A,Value2B,Value2C
    Value3A,Value3B,Value3C
    Value4A,Value4B,Value4C
    Value5A,Value5B,Value5C
    Value6A,Value6B,Value6C
    Value7A,Value7B,Value7C
    Value8A,Value8B,Value8C
    Value9A,Value9B,Value9C
    Value10A,Value10B,Value10C
    Value11A,Value11B,Value11C
    Value12A,Value12B,Value12C
    Value13A,Value13B,Value13C
    Value14A,Value14B,Value14C
    Value15A,Value15B,Value15C`;
    
    // Split the CSV data
    let rows = testData.split("n");
    
    // Add table tags to each row and cell
    let table = "";
    rows.forEach(function(row) {
      const columns = row.split(/,(?=(?:[^"]*"[^"]*")*(?![^"]*"))/);
      table += `<tr>`;
    
      // Iterate over each column
      columns.forEach(function(column) {
        table += "<td>" + column.replace(/^"(.*)"$/, "$1") + "</td>";
      });
    
      table += "</tr>";
    });
    
    // Update the table body
    const tbody = document.querySelector("#csv-table tbody");
    tbody.innerHTML = table;
    * {
      box-sizing: border-box;
    }
    
    body {
      margin: 0;
      padding: 0;
      font-family: Arial, sans-serif;
      background-color: #F3F2F1;
      background-color: #d9efa1;
    }
    
    .container {
      display: flex;
      flex-direction: column;
      justify-content: center;
      max-width: 1024px;
      margin: 0 auto;
      padding: 5%;
    }
    
    .csv {
      background-color: #FFFFFF;
      border-radius: 10px;
      padding: 3%;
      box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
      max-height: 95vh;
      overflow: auto;
    }
    
    .csv h2 {
      margin-top: 1%;
      margin-bottom: 3%;
      margin-left: 0.1%;
      color: #f6b40e;
    }
    
    .csv table {
      width: 100%;
      border-collapse: collapse;
      table-layout: fixed;
    }
    
    .csv th,
    .csv td {
       padding: 5px;
       text-align: left;
       width: 400px;
     }
    
    
    .csv table thead th {
      background-color: #f6b40e;
      color: #FFFFFF;
      padding: 3%;
      text-align: left;
    }
    
    .csv thead tr {
        display: block;
    }
    
    .csv table tbody tr:nth-child(even) {
      background-color: #F3F2F1;
    }
    
    .csv table tbody td {
      padding: 3%;
      word-wrap: break-word;
    }
    
    .csv tbody {
        display: block;
        overflow: auto;
        width: 100%;
        height: 100px;
    }
    <!DOCTYPE html>
    <html>
    
      <head>
        <title>CSV</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
      </head>
    
      <body>
        <div class="container">
          <div class="csv">
            <h2>CSV</h2>
            <table id="csv-table">
              <thead>
                <tr>
                  <th>A</th>
                  <th>B</th>
                  <th>C</th>
                </tr>
              </thead>
              <tbody>
                <!-- Dynamically generated rows will be added here -->
              </tbody>
            </table>
          </div>
        </div>
      </body>
    
    </html>
    Login or Signup to reply.
  2. You should be able to use position: sticky with an appropriate top value. You may also wish to consider having some sort of background color to prevent the scrolled values showing above the table header.

    // Define CSV data here
    const testData = `Value1A,Value1B,Value1C
    Value2A,Value2B,Value2C
    Value3A,Value3B,Value3C
    Value4A,Value4B,Value4C
    Value5A,Value5B,Value5C
    Value6A,Value6B,Value6C
    Value7A,Value7B,Value7C
    Value8A,Value8B,Value8C
    Value9A,Value9B,Value9C
    Value10A,Value10B,Value10C
    Value11A,Value11B,Value11C
    Value12A,Value12B,Value12C
    Value13A,Value13B,Value13C
    Value14A,Value14B,Value14C
    Value15A,Value15B,Value15C`;
    
    // Split the CSV data
    let rows = testData.split("n");
    
    // Add table tags to each row and cell
    let table = "";
    rows.forEach(function(row) {
      const columns = row.split(/,(?=(?:[^"]*"[^"]*")*(?![^"]*"))/);
      table += `<tr>`;
    
      // Iterate over each column
      columns.forEach(function(column) {
        table += "<td>" + column.replace(/^"(.*)"$/, "$1") + "</td>";
      });
    
      table += "</tr>";
    });
    
    // Update the table body
    const tbody = document.querySelector("#csv-table tbody");
    tbody.innerHTML = table;
    * {
      box-sizing: border-box;
    }
    
    body {
      margin: 0;
      padding: 0;
      font-family: Arial, sans-serif;
      background-color: #F3F2F1;
      background-color: #d9efa1;
    }
    
    .container {
      display: flex;
      flex-direction: column;
      justify-content: center;
      max-width: 1024px;
      margin: 0 auto;
      padding: 5%;
    }
    
    .csv {
      background-color: #FFFFFF;
      border-radius: 10px;
      padding: 3%;
      box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
      max-height: 95vh;
      overflow: auto;
    }
    
    .csv h2 {
      margin-top: 1%;
      margin-bottom: 3%;
      margin-left: 0.1%;
      color: #f6b40e;
    }
    
    .csv table {
      width: 100%;
      border-collapse: collapse;
      table-layout: fixed;
    }
    
    .csv table thead th {
      background-color: #f6b40e;
      color: #FFFFFF;
      padding: 3%;
      text-align: left;
    }
    
    .csv table tbody tr:nth-child(even) {
      background-color: #F3F2F1;
    }
    
    .csv table tbody td {
      padding: 3%;
      word-wrap: break-word;
    }
    <!DOCTYPE html>
    <html>
    
    <head>
      <title>CSV</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    
    <body>
      <div class="container">
        <div class="csv">
          <h2 style="position: sticky; top: calc(min(1024px, 100vw) * 0.0084)">CSV</h2>
          <table id="csv-table">
            <thead style="position: sticky; top: calc((min(1024px, 100vw) * 0.0336) + 1.75em)">
              <tr>
                <th>A</th>
                <th>B</th>
                <th>C</th>
              </tr>
            </thead>
            <tbody>
              <!-- Dynamically generated rows will be added here -->
            </tbody>
          </table>
        </div>
      </div>
    </body>
    
    </html>

    To explain the top values I’ve used here:

    calc(min(1024px, 100vw) * 0.0084) is derived from margin-top: 1% is 1% of the width of the containing box. However, the percentage for top uses the height so we cannot apply top: 1%, so instead, we need to calculate what margin-top: 1% is:

    • min(1024px, 100vw) is the .container‘s width, which is implicitly 100% of the viewport, 100vw up to its max-width: 1024px, the 1024px.
    • The 0.0084 is derived from:
      • There is left and right padding of .container of 5% each, that is 0.05 each side.
      • There is left and right padding of .csv of 3% each, that is 0.03 each side.
      • There is the 1% margin-top itself, equal to 0.01.
      • So the multiplicative factor is:
        (1 - (0.05 × 2) - (0.03 × 2)) × 0.01 = 0.84 × 0.01 = 0.0084
        

    For calc((min(1024px, 100vw) * 0.0336) + 1.75em), it is mostly the same:

    • min(1024px, 100vw) is the .container‘s width, which is implicitly 100% of the viewport, 100vw up to its max-width: 1024px, the 1024px.
    • The 0.0084 is derived from:
      • There is left and right padding of .container of 5% each, that is 0.05 each side.
      • There is left and right padding of .csv of 3% each, that is 0.03 each side.
      • There is the 1% margin-top of the <h2>, equal to 0.01.
      • There is the 3% margin-bottom of the <h2>, equal to 0.03.
      • So the multiplicative factor is:
        (1 - (0.05 × 2) - (0.03 × 2)) × 0.04 = 0.84 × 0.04 = 0.0336
        
    • 1.75em comes from the line-height of the <h2> text.

    If you want to be super accurate, we’d need to use JavaScript. This is because non-overlay scrollbars can give us slightly different values, and we do not have the necessary values in CSS alone to calculate it properly.

    // Define CSV data here
    const testData = `Value1A,Value1B,Value1C
    Value2A,Value2B,Value2C
    Value3A,Value3B,Value3C
    Value4A,Value4B,Value4C
    Value5A,Value5B,Value5C
    Value6A,Value6B,Value6C
    Value7A,Value7B,Value7C
    Value8A,Value8B,Value8C
    Value9A,Value9B,Value9C
    Value10A,Value10B,Value10C
    Value11A,Value11B,Value11C
    Value12A,Value12B,Value12C
    Value13A,Value13B,Value13C
    Value14A,Value14B,Value14C
    Value15A,Value15B,Value15C`;
    
    // Split the CSV data
    let rows = testData.split("n");
    
    // Add table tags to each row and cell
    let table = "";
    rows.forEach(function(row) {
      const columns = row.split(/,(?=(?:[^"]*"[^"]*")*(?![^"]*"))/);
      table += `<tr>`;
    
      // Iterate over each column
      columns.forEach(function(column) {
        table += "<td>" + column.replace(/^"(.*)"$/, "$1") + "</td>";
      });
    
      table += "</tr>";
    });
    
    // Update the table body
    const tbody = document.querySelector("#csv-table tbody");
    tbody.innerHTML = table;
    
    const tableElement = document.getElementById('csv-table');
    const { width } = tableElement.getBoundingClientRect();
    
    document
      .querySelector('.csv')
      .style.setProperty('--w', `${width}px`);
    * {
      box-sizing: border-box;
    }
    
    body {
      margin: 0;
      padding: 0;
      font-family: Arial, sans-serif;
      background-color: #F3F2F1;
      background-color: #d9efa1;
    }
    
    .container {
      display: flex;
      flex-direction: column;
      justify-content: center;
      max-width: 1024px;
      margin: 0 auto;
      padding: 5%;
    }
    
    .csv {
      background-color: #FFFFFF;
      border-radius: 10px;
      padding: 3%;
      box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
      max-height: 95vh;
      overflow: auto;
    }
    
    .csv h2 {
      margin-top: 1%;
      margin-bottom: 3%;
      margin-left: 0.1%;
      color: #f6b40e;
    }
    
    .csv table {
      width: 100%;
      border-collapse: collapse;
      table-layout: fixed;
    }
    
    .csv table thead th {
      background-color: #f6b40e;
      color: #FFFFFF;
      padding: 3%;
      text-align: left;
    }
    
    .csv table tbody tr:nth-child(even) {
      background-color: #F3F2F1;
    }
    
    .csv table tbody td {
      padding: 3%;
      word-wrap: break-word;
    }
    <!DOCTYPE html>
    <html>
    
    <head>
      <title>CSV</title>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    
    <body>
      <div class="container">
        <div class="csv">
          <h2 style="position: sticky; top: calc(var(--w) * 0.01)">CSV</h2>
          <table id="csv-table">
            <thead style="position: sticky; top: calc((var(--w) * 0.04) + 1.75em)">
              <tr>
                <th>A</th>
                <th>B</th>
                <th>C</th>
              </tr>
            </thead>
            <tbody>
              <!-- Dynamically generated rows will be added here -->
            </tbody>
          </table>
        </div>
      </div>
    </body>
    
    </html>
    Login or Signup to reply.
  3. As explained by Wongjn: position sticky can help.

    But you can also wrap your <table> in a <div> with a max-height and overflow auto and give your <thead> a sticky position like so:

    * {
      box-sizing: border-box;
    }
    
    .table-wrp {
      max-height:70vh;
      overflow:auto;
    }
    
    
    thead{
      position:sticky;
      top:0
    }
    
    body {
      margin: 0;
      padding: 0;
      font-family: Arial, sans-serif;
      background-color: #F3F2F1;
      background-color: #d9efa1;
    }
    
    .container {
      display: flex;
      flex-direction: column;
      justify-content: center;
      max-width: 1024px;
      margin: 0 auto;
      padding: 5%;
    }
    
    .csv {
      background-color: #FFFFFF;
      border-radius: 10px;
      padding: 3%;
      box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
      //max-height: 95vh;
      //overflow: auto;
    }
    
    .csv h2 {
      margin-top: 1%;
      margin-bottom: 3%;
      margin-left: 0.1%;
      color: #f6b40e;
    }
    
    .csv table {
      width: 100%;
      border-collapse: collapse;
      table-layout: fixed;
    }
    
    .csv table thead th {
      background-color: #f6b40e;
      color: #FFFFFF;
      padding: 3%;
      text-align: left;
    }
    
    .csv table tbody tr:nth-child(even) {
      background-color: #F3F2F1;
    }
    
    .csv table tbody td {
      padding: 3%;
      word-wrap: break-word;
    }
    <div class="container">
      <div class="csv">
        <h2>CSV</h2>
        <div class="table-wrp">
        <table id="csv-table">
          <thead>
            <tr>
              <th>A</th>
              <th>B</th>
              <th>C</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>Value1A</td>
              <td>Value1B</td>
              <td>Value1C</td>
            </tr>
            <tr>
              <td>Value2A</td>
              <td>Value2B</td>
              <td>Value2C</td>
            </tr>
            <tr>
              <td>Value3A</td>
              <td>Value3B</td>
              <td>Value3C</td>
            </tr>
            <tr>
              <td>Value4A</td>
              <td>Value4B</td>
              <td>Value4C</td>
            </tr>
            <tr>
              <td>Value5A</td>
              <td>Value5B</td>
              <td>Value5C</td>
            </tr>
            <tr>
              <td>Value6A</td>
              <td>Value6B</td>
              <td>Value6C</td>
            </tr>
            <tr>
              <td>Value7A</td>
              <td>Value7B</td>
              <td>Value7C</td>
            </tr>
            <tr>
              <td>Value8A</td>
              <td>Value8B</td>
              <td>Value8C</td>
            </tr>
            <tr>
              <td>Value9A</td>
              <td>Value9B</td>
              <td>Value9C</td>
            </tr>
            <tr>
              <td>Value10A</td>
              <td>Value10B</td>
              <td>Value10C</td>
            </tr>
            <tr>
              <td>Value11A</td>
              <td>Value11B</td>
              <td>Value11C</td>
            </tr>
            <tr>
              <td>Value12A</td>
              <td>Value12B</td>
              <td>Value12C</td>
            </tr>
            <tr>
              <td>Value13A</td>
              <td>Value13B</td>
              <td>Value13C</td>
            </tr>
            <tr>
              <td>Value14A</td>
              <td>Value14B</td>
              <td>Value14C</td>
            </tr>
            <tr>
              <td>Value15A</td>
              <td>Value15B</td>
              <td>Value15C</td>
            </tr>
          </tbody>
        </table>
          </div>
      </div>
    </div>

    We need this additional wrap because a sticky parent element with overflow auto or scroll will disable the sticky behavior.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search