Improving responsive data table UX with CSS
Tags: css, webdev, ux
Published: 2023-09-14 14:13:33

**Written by [Timonwa Akintokun](https://blog.logrocket.com/author/pelumiakintokun/)✏️**

Data tables are widely used in web design to present organized and structured data. However, when they contain large amounts of data, it can be challenging to optimize for user experience.

When data tables are embedded within a responsive web design, the problem becomes even more complex. To ensure that tables are displayed correctly on all devices, developers often resort to hiding table columns on smaller screens. This can be a frustrating experience for users, who must resort to scrolling horizontally or zooming out to access the hidden data. 

There are several basic strategies we can use to build responsive data tables. In this article, we’ll review some additional approaches we can take to further optimize responsive data table UX with CSS. 

_Jump ahead:_

*   [Freezing or fixing rows or columns](#freezing-fixing-rows-columns)
*   [Implementing a stacking approach (row to blocks)](#implementing-stacking-approach)
    *   [Adding accordions to stacked rows](#adding-accordions-stacked-rows)
*   [Selectively hiding and toggling column visibility](#selectively-hiding-toggling-column-visibility)
    *   [Collapsing rows](#collapsing-rows)
*   [Data grouping](#data-grouping)
*   [Data prioritization](#data-prioritization)

## Freezing or fixing rows or columns <a name="freezing-fixing-rows-columns">

One of the major challenges associated with tables containing large amounts of data is user difficulty in maintaining visibility and context, particularly when scrolling horizontally or vertically. Fixed rows and columns within the table are beneficial for addressing this issue, helping to ensure that important information remains visible at all times. 

For example, a fixed header and footer (i.e., the first and last rows of the table), provide users with continuous access to essential data points while scrolling through the rest of the table's content. This approach provides a consistent reference point and prevents the loss of context, enabling users to analyze and interpret data more effectively. 

We can implement fixed headers and footers using CSS styling and the `position: sticky` property. When we assign this property to the table's header and footer elements, we ensure their fixed position at the top and bottom of the table, respectively. This way, those rows remain visible even when the user is scrolling through extensive data sets:

```css
.table-wrapper {
  max-height: 500px;
  overflow-y: scroll;
}
thead {
  position: sticky;
  top: 0;
  z-index: 2;
}
tfoot {
  position: sticky;
  bottom: 0;
  z-index: 2;
}
```

Here, we set the `.table-wrapper` class to have a maximum height of `500px` and the `overflow-y` property to `scroll` to enable vertical scrolling. We also set the `position` property to `sticky` for the `thead` and `tfoot` elements, the `top` property to `0` for the `thead` element, and the `bottom` property to `0` for the `tfoot` element. This ensures that the header and footer rows always remain fixed at the top and bottom of the table, regardless of scrolling: [caption id="attachment_177379" align="aligncenter" width="720"]![Table Fixed Header Footer](https://blog.logrocket.com/wp-content/uploads/2023/09/table-fixed-header-footer.gif)

We can also fix the first set of columns. This is helpful for columns that contain identifying data, like names or serial numbers. With fixed columns, users can maintain a reference point while scrolling horizontally through the remaining data:

```html
<thead>
  <tr>
    <th>ID</th>
    <th class="fixed-column">Name</th>
    ...
    <!-- rest of the data -->
  </tr>
</thead>
<tbody>
  <tr>
    <td>1</td>
    <td class="fixed-column">John Doe</td>
    <td>johndoe@example.com</td>
    ...
    <!-- rest of the data -->
  </tr>
</tbody>
```

```css
.fixed-column {
  position: sticky;
  left: 0;
  z-index: 1;
}
td.fixed-column {
  background-color: #fff;
}
```

Here, we add the `fixed-column` class name to the column holding the names. The `position` property is set to `sticky` for the `fixed-column` class, and the `left` property is set to `0` to ensure that the column remains fixed on the left side of the table. 

The `z-index` property is set to `1` to ensure that the column remains visible even when scrolling through the rest of the table's content. We set the column’s `background-color` property to black (`# ffff`) to ensure it remains consistent with the rest of the table. 

By fixing a particular set of columns and rows, we ensure that important information remains visible and accessible at all times, providing a consistent reference point, avoiding loss of context, and enabling users to analyze and interpret data more effectively:

See the Pen [having fixed column(s) and rows](https://codepen.io/timonwa/pen/XWxwdeW) by Timonwa ([@timonwa/](https://codepen.io/timonwa/)) on [CodePen](https://codepen.io).

## Implementing a stacking approach (rows to blocks) <a name="implementing-stacking-approach">

The stacking approach is a technique commonly used in responsive web design to enhance the readability and usability of tables on smaller screens. It involves transforming the rows of a table into vertically stacked blocks or cards, making it easier for users to scroll through and consume tabular data on mobile devices. 

We can utilize CSS to modify the table's layout and presentation on smaller screens to implement the stacking approach. Here's an example implementation using CSS:

```html
<table>
  ...
  <tbody>
    <tr>
      <td name="S/N">1</td>
      <td name="Name">John Doe</td>
      <td name="Email">johndoe@example.com</td>
      <td name="Phone">123-456-7890</td>
      <td name="Address">123 Main St</td>
      <td name="City">New York</td>
      <td name="Age">35</td>
      <td name="Occupation">Software Engineer</td>
      <td name="Company">ABC Corporation</td>
      <td name="Salary">$80,000</td>
      <td name="Department">Engineering</td>
      <td name="Join Date">2022-01-01</td>
      <td name="Notes"
        >Lorem ipsum dolor sit amet, consectetur adipiscing elit.</td
      >
    </tr>
    <!-- rest of the table -->
  </tbody>
  ...
</table>
```

```css
@media screen and (max-width: 768px) {
  table {
    width: 300px;
  }
  thead {
    display: none;
  }
  tr {
    display: block;
    margin-bottom: 20px;
    border-radius: 5px;
    border: 1px solid #ddd;
  }
  tr td {
    display: block;
    width: 100%;
    position: relative;
    white-space: pre-wrap;
    border: none;
    border-bottom: 1px solid #ddd;
  }
  tr td:first-child {
    display: none;
  }
  tr td:nth-child(2) {
    background-color: #4caf50;
    color: white;
    padding: 8px;
    border-radius: 5px 5px 0 0;
  }
  tr td:not(:nth-child(2))::before {
    content: attr(name);
    position: absolute;
    top: 8px;
    left: 0;
    font-weight: bold;
    text-align: left;
    width: 100%;
    padding: 0 8px;
  }
}
```

In the HTML code, we add a `name` attribute to each `<td>` element. This attribute is used to display the labels for the data blocks. 

In the CSS code, we use a media query to target screens with a maximum width of `768px`. Inside the media query, we apply the following CSS rules:

*   `table`: The table has a width of `300px` to ensure it fits well on smaller screens
*   `thead`: The table header is hidden as it's not necessary for the stacking approach
*   `tr`: Each table row is displayed as a block element with a bottom margin of `20px` to create spacing between the blocks. The rows also have a `1px` solid border for visual separation. The `border-radius` property is used to create rounded corners for the top edges of the blocks
*   `tr td`: Each table cell is displayed as a block element with a width of `100%`, causing the cells to stack vertically. `position: relative` is used to position the label text with `position: absolute`
*   `tr td:first-child`: The first cell in each row (in this example, “Serial Number”) is hidden using `display: none` since it's not required in the stacked layout
*   `tr td:nth-child(2)`: The second cell in each row (in this example, “Name”) has a green background with white text. It has a top border radius of `5px` to create a rounded top edge
*   `tr td:not(:nth-child(2))::before`: For cells other than the second cell in each row, a `::before` pseudo-element is used to display the label text (the `name` attribute) as a bold header above the cell content. It's positioned absolutely at the top left corner of the cell

Applying this CSS code transforms the table into a vertically stacked layout on smaller screens, allowing users to more easily scroll through the tabular data. The stacking approach improves the table's accessibility and UX on mobile devices:

See the Pen [The stacking approach (rows to blocks) by Timonwa (](https://codepen.io/timonwa/pen/QWZRBxb)[@timonwa/](https://codepen.io/timonwa/)[) on](https://codepen.io/timonwa/pen/QWZRBxb) [CodePen](https://codepen.io)[.](https://codepen.io/timonwa/pen/QWZRBxb)

### Adding accordions to stacked rows <a name="adding-accordions-stacked-rows">

We can take the stacking approach a step further by turning each block into an accordion. This allows users to expand and collapse the blocks to view the data they need. This is especially useful when handling large amounts of data, as it allows users to focus on specific information without having to scroll through the entire table. This can be achieved using JavaScript and more CSS:

```html
<table>
  ...
  <tbody>
    <tr class="active">
      <td name="S/N">1</td>
      <td name="Name">John Doe</td>
      <td name="Email">johndoe@example.com</td>
      <td name="Phone" class="accordion-content">123-456-7890</td>
      <td name="Address" class="accordion-content">123 Main St</td>
      <td name="City" class="accordion-content">New York</td>
      <td name="Age" class="accordion-content">35</td>
      <td name="Occupation">Software Engineer</td>
      <td name="Company" class="accordion-content">ABC Corporation</td>
      <td name="Salary" class="accordion-content">$80,000</td>
      <td name="Department" class="accordion-content">Engineering</td>
      <td name="Join Date" class="accordion-content">2022-01-01</td>
      <td name="Notes" class="accordion-content"
        >Lorem ipsum dolor sit amet, consectetur adipiscing elit.</td
      >
    </tr>
    <tr>
      <td name="S/N">1</td>
      <td name="Name" class="fixed-column">John Doe</td>
      <td name="Email">johndoe@example.com</td>
      <td name="Phone" class="accordion-content">123-456-7890</td>
      ...
      <!-- rest of the table -->
    </tr></tbody
  >
  ...
</table>
```

```css
tr {
  cursor: pointer;
}
tr .accordion-content {
  display: none;
}
tr.active .accordion-content {
  display: block;
}
```

```javascript
document.addEventListener("DOMContentLoaded", function () {
  var accordionRows = document.querySelectorAll("tr");
  accordionRows.forEach(function (row) {
    row.addEventListener("click", function () {
      this.classList.toggle("active");
    });
  });
});
```

Here’s how our sample table looks using the stacking approach with accordions:

See the Pen [The stacking approach plus accordion by Timonwa (](https://codepen.io/timonwa/pen/KKGLYam)[@timonwa/](https://codepen.io/timonwa/)[) on](https://codepen.io/timonwa/pen/KKGLYam) [CodePen](https://codepen.io)[.](https://codepen.io/timonwa/pen/KKGLYam)

In the code snippets above, we add the `accordion-content` class to the cells that we want to hide. We also add the `active` class to the row we want to expand by default. In the CSS code, we add some styles to hide and display the content when the row is active. In the JavaScript code, we add an event listener to toggle the `active` class when the row is clicked.

## Selectively hiding and toggling column visibility <a name="selectively-hiding-toggling-column-visibility">

To optimize tables for smaller screens and enhance their responsiveness, we can selectively hide columns based on the screen size and toggle their visibility using JavaScript. Hiding specific columns is beneficial for various reasons. For instance, it can prevent horizontal scrolling on smaller screens by hiding columns with extensive text or large amounts of data. 

This approach ensures that the table fits within the available screen width and provides a seamless user experience. Additionally, we can hide columns that contain nonessential information, or elements like buttons or icons, reducing visual noise on smaller screens. 

To implement this technique effectively, we can utilize CSS to hide the desired columns on smaller screens and then leverage JavaScript to toggle the visibility of these columns based on the screen size. 

The following example demonstrates an implementation of this approach using CSS and JavaScript:

```html
<div class="checkboxes">
  <label for="togglePhone">
    <input type="checkbox" id="togglePhone" />Phone
  </label>
  <label for="toggleAddress">
    <input type="checkbox" id="toggleAddress" />Address
  </label>
  <label for="toggleCity">
    <input type="checkbox" id="toggleCity" />City
  </label>
  <label for="toggleAge"> <input type="checkbox" id="toggleAge" />Age </label>
  <label for="toggleOccupation">
    <input type="checkbox" id="toggleOccupation" />Occupation
  </label>
  <label for="toggleCompany">
    <input type="checkbox" id="toggleCompany" />Company
  </label>
  <label for="toggleSalary">
    <input type="checkbox" id="toggleSalary" />Salary
  </label>
  <label for="toggleDepartment">
    <input type="checkbox" id="toggleDepartment" />Department
  </label>
  <label for="toggleJoin-date">
    <input type="checkbox" id="toggleJoin-date" />Join Date
  </label>
  <label for="toggleNotes">
    <input type="checkbox" id="toggleNotes" />Notes
  </label>
</div>
<div class="table-wrapper">
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Email</th>
        <th class="phone">Phone</th>
        <th class="address">Address</th>
        <th class="city">City</th>
        <th class="age">Age</th>
        <th class="occupation">Occupation</th>
        <th class="company">Company</th>
        <th class="salary">Salary</th>
        <th class="department">Department</th>
        <th class="join-date">Join Date</th>
        <th class="notes">Notes</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>1</td>
        <td>John Doe</td>
        <td>johndoe@example.com</td>
        <td class="phone">123-456-7890</td>
        <td class="address">123 Main St</td>
        <td class="city">New York</td>
        <td class="age">35</td>
        <td class="occupation">Software Engineer</td>
        <td class="company">ABC Corporation</td>
        <td class="salary">$80,000</td>
        <td class="department">Engineering</td>
        <td class="join-date">2022-01-01</td>
        <td class="notes"
          >Lorem ipsum dolor sit amet, consectetur adipiscing elit.</td
        >
      </tr>
      ...
      <!-- rest of the table -->
    </tbody>
  </table>
</div>
```

```css
.checkboxes {
  display: none;
  justify-content: flex-start;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
  margin-bottom: 20px;
}
@media screen and (max-width: 768px) {
  .checkboxes {
    display: flex;
  }
  th.phone,
  td.phone,
  th.address,
  td.address,
  th.city,
  td.city,
  th.age,
  td.age,
  th.occupation,
  td.occupation,
  th.company,
  td.company,
  th.salary,
  td.salary,
  th.department,
  td.department,
  th.join-date,
  td.join-date,
  th.notes,
  td.notes {
    display: none;
  }
}
```

```javascript
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach((checkbox) => {
  checkbox.addEventListener("change", (event) => {
    const columnClass = event.target.id.replace("toggle", "").toLowerCase();
    const columnCells = document.querySelectorAll(`td.${columnClass}`);
    const columnHeads = document.querySelectorAll(`th.${columnClass}`);
    if (event.currentTarget.checked) {
      columnCells.forEach((cell) => {
        cell.style.display = "table-cell";
      });
      columnHeads.forEach((cell) => {
        cell.style.display = "table-cell";
      });
    } else {
      columnCells.forEach((cell) => {
        cell.style.display = "none";
      });
      columnHeads.forEach((cell) => {
        cell.style.display = "none";
      });
    }
  });
});
```

We create the checkboxes and a class in the HTML code for each column that we want to hide. In the CSS, we add some styles to hide the checkboxes on larger screens and display them on smaller screens. We also add some styles to hide the columns on smaller screens. 

In the JavaScript code, we select all the checkboxes and add an event listener to each checkbox. When a checkbox is checked, we get its `id` and save it to the `columnClass` variable. 

We also select and display all cells and headers that have a class name equal to the `columnClass` variable. When a checkbox is unchecked, we select and hide all cells and headers that have a class name equal to the `columnClass` variable from the checkbox `id`. 

Here’s our approach for selectively hiding and toggling column visibility in action:

See the Pen [Selectively hiding and toggling column visibility by Timonwa (](https://codepen.io/timonwa/pen/JjmqqVR)[@timonwa/](https://codepen.io/timonwa/)[) on](https://codepen.io/timonwa/pen/JjmqqVR) [CodePen](https://codepen.io)[.](https://codepen.io/timonwa/pen/JjmqqVR)

### Collapsing rows

The technique of using collapsible rows is an alternative approach for optimizing tables for smaller screens. Rather than stacking rows or hiding columns, this method focuses on collapsing rows to save space and provide a more condensed view of the table. It involves displaying a summary row for each data entry, containing essential information in a condensed format. 

By initially presenting only the summarized information, space is conserved on smaller screens. Users can then expand a specific row to reveal its full details using an expand/collapse button, enabling access to more comprehensive data when needed. 

Collapsing rows reduces the vertical space occupied by the table on smaller screens, making it easier for users to scroll through and find the desired information. This approach is particularly useful for tables with a large number of rows as they allow users to focus initially on the essential details and selectively explore additional information as required. 

Implementing collapsible rows typically involves using JavaScript or CSS to toggle the visibility or height of the expanded content when the expand/collapse button is clicked. This dynamic adjustment of the table's display based on user interactions provides a responsive and space-efficient layout for smaller screens. 

Whereas the stacking approach rearranges the table's content vertically, displaying one row per screen, the collapsible rows approach condenses the table vertically by presenting summary rows and allowing users to expand individual rows for more detailed information. Both techniques aim to enhance the usability of tables on smaller screens, but they differ in their presentation and interaction patterns. 

Here's an example implementation of collapsible rows using JavaScript and CSS:

```html
<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Age</th>
      <th>Occupation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        John Doe
        <button class="expand-btn" onclick="toggleDetails(this)"></button>
      </td>
      <td>35</td>
      <td>Software Engineer</td>
    </tr>
    <tr class="details">
      <td colspan="3">
        <p>Email: johndoe@example.com</p>
        <p>Phone: 123-456-7890</p>
        <p>Address: 123 Main St</p>
        <p>City: New York</p>
        <!-- Additional details can be included here -->
      </td>
    </tr>
    <tr>
      <td>
        Jane Smith
        <button class="expand-btn" onclick="toggleDetails(this)"></button>
      </td>
      <td>42</td>
      <td>Marketing Manager</td>
    </tr>
    <tr class="details">
      <td colspan="3">
        <p>Email: janesmith@example.com</p>
        <p>Phone: 987-654-3210</p>
        <p>Address: 456 Park Ave</p>
        <!-- Additional details can be included here -->
      </td>
    </tr>
    <!-- Add more rows as needed -->
  </tbody>
</table>
```

```css
.table-wrapper {
  width: 100%;
  max-width: 500px;
  overflow-x: auto;
}
table {
  width: 100%;
  border-collapse: collapse;
}
.details {
  display: none;
}
.expand-btn::after {
  content: "+";
  margin: 5px;
}
.collapse-btn::after {
  content: "-";
  margin: 5px;
}
```

```javascript
function toggleDetails(button) {
  const detailsRow = button.parentNode.parentNode.nextElementSibling;
  detailsRow.classList.toggle("details");
  button.classList.toggle("expand-btn");
  button.classList.toggle("collapse-btn");
}
```

Here’s the resulting table with collapsible rows:

See the Pen [Collapsing rows by Timonwa (](https://codepen.io/timonwa/pen/BaqgQxY)[@timonwa/](https://codepen.io/timonwa/)[) on](https://codepen.io/timonwa/pen/BaqgQxY) [CodePen](https://codepen.io)[.](https://codepen.io/timonwa/pen/BaqgQxY)

## Data grouping

Grouping related columns and displaying them within a single cell or expanding panel reduces the table's width and simplifies the display, making it more manageable on smaller screens while maintaining the organization and structure of the data. 

Let's look at an example to illustrate this technique. Let’s say we have a table displaying customer orders, with columns for Order ID, Customer Name, Email, Phone, Shipping Address, Product, Price, Quantity, Order Date, Order Status, Additional Notes, and a link to the product page. 

Initially, on large screens, the data is displayed in separate columns, but on smaller screens, we can group the customer-related information and the product-related information into a single cell. This reduces the table's width and simplifies the display, making it more manageable on smaller screens while maintaining the organization and structure of the data. 

By employing data grouping, we simplify the display of tables on smaller screens by reducing their width and condensing related columns. Users can expand the groups as needed to view additional information, allowing for a more compact and organized representation of the data:

See the Pen [Data grouping by Timonwa (](https://codepen.io/timonwa/pen/VwEOJBB)[@timonwa/](https://codepen.io/timonwa/)[) on](https://codepen.io/timonwa/pen/VwEOJBB) [CodePen](https://codepen.io)[.](https://codepen.io/timonwa/pen/VwEOJBB)

## Data prioritization

Data prioritization is a concept that can be applied to any of the previously mentioned approaches. Identifying the most important data points and prioritizing them is an effective way to make a table more responsive. 

It's important to consider the context and purpose of the table when deciding which data points to prioritize. By prioritizing the most important data points, we can ensure that they are always visible and accessible to users, regardless of the screen size or device they are using. This helps maintain the table's usability and readability on smaller screens, ensuring that users can easily access the information they need. 

It’s critical to evaluate the specific requirements and priorities of the data being presented before deciding which data points to prioritize. It is also important to understand that all these techniques can be combined to create an even more responsive table. For example, we can use the stacking approach to rearrange the table's content vertically and then use data grouping to combine related columns into a single cell.

## Conclusion

Improving the user experience of responsive data tables enables u9\. In this article, we reviewed several strategies for improving responsive data table UX: freezing or fixing rows or columns, implementing a stacking approach (both with and without accordions), selectively hiding and toggling column visibility (with an option to collapse rows), data grouping, and data prioritization. 

You can [build responsive data tables in CSS](https://blog.logrocket.com/creating-responsive-data-tables-css/) and then adopt some or all of the approaches shared in this article to further improve user experience. You can view and play with all the code snippets from this article on [CodePen](https://codepen.io/collection/VYBVQq).

---

## Is your frontend hogging your users' CPU?

As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, [try LogRocket](https://lp.logrocket.com/blg/css-signup).

[![LogRocket Signup](https://blog.logrocket.com/wp-content/uploads/2019/12/cpu-memory-usage.png)](https://lp.logrocket.com/blg/css-signup)

[LogRocket](https://lp.logrocket.com/blg/css-signup) is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.

Modernize how you debug web and mobile apps — [Start monitoring for free](https://lp.logrocket.com/blg/css-signup).