In the modern digital era, where data is growing faster than ever, efficiently handling and displaying complex tabular data becomes crucial for the UI. Whether it’s financial reports, inventory management, or extensive datasets for machine learning, the ability to scroll through large amounts of data without losing context is essential. This is where the concept of sticky headers and columns comes into play, providing a fixed point of reference that enhances readability and user experience.
In this tutorial, we will build a React component that features a table with sticky headers and columns. This means that the header row and specific columns will remain visible on the screen even as we scroll vertically or horizontally through our dataset.
Creating a Scrollable Table in React
To start, we’ll build a basic scrollable table in React that displays student information. This table will dynamically populate rows based on a dataset and be styled to support both vertical and horizontal scrolling. Let’s break down the steps and write the necessary code.
Step 1: Creating a Basic Table Component
import "./App.css";
const students = [
 {
  id: 1,
  name: "John Doe",
  age: 20,
  course: "Business",
  grade: "A",
  attendance: "Present",
  status: "Active",
  remarks: "Good",
 },
 {
  id: 2,
  name: "Jane Smith",
  age: 22,
  course: "Engineering",
  grade: "B",
  attendance: "Absent",
  status: "Inactive",
  remarks: "Average",
 },
 {
  id: 3,
  name: "Alice Johnson",
  age: 21,
  course: "Medicine",
  grade: "A+",
  attendance: "Present",
  status: "Active",
  remarks: "Excellent",
 },
 {
  id: 4,
  name: "Bob Brown",
  age: 23,
  course: "Computer Science",
  grade: "C",
  attendance: "Present",
  status: "Active",
  remarks: "Fair",
 },
 {
  id: 5,
  name: "Eve Wilson",
  age: 24,
  course: "Arts",
  grade: "B+",
  attendance: "Absent",
  status: "Inactive",
  remarks: "Good",
 },
 {
  id: 6,
  name: "David Lee",
  age: 25,
  course: "Law",
  grade: "A-",
  attendance: "Present",
  status: "Active",
  remarks: "Very Good",
 },
 {
  id: 7,
  name: "Grace Clark",
  age: 26,
  course: "Science",
  grade: "D",
  attendance: "Absent",
  status: "Inactive",
  remarks: "Poor",
 },
];
const TableComponent = () => {
 return (
  <table>
   <thead>
    <tr>
     <th>ID</th>
     <th>Name</th>
     <th>Age</th>
     <th>Course</th>
     <th>Grade</th>
     <th>Attendance</th>
     <th>Status</th>
     <th>Remarks</th>
    </tr>
   </thead>
   <tbody>
    {students.map(student => (
     <tr key={student.id}>
      <td>{student.id}</td>
      <td>{student.name}</td>
      <td>{student.age}</td>
      <td>{student.course}</td>
      <td>{student.grade}</td>
      <td>{student.attendance}</td>
      <td>{student.status}</td>
      <td>{student.remarks}</td>
     </tr>
    ))}
   </tbody>
  </table>
 );
};
export default TableComponent;
In this code:
- We define a simple array of objects containing student data.
TableComponent
outputs a table where we use.map()
to create a row for each student.
Let’s add some styling to make the table visually appealing:
table {
 width: 100%;
 border-collapse: collapse;
}
th,
td {
 padding: 8px;
 text-align: left;
 border-bottom: 1px solid #ddd;
}
thead th {
 background-color: #f2f2f2;
}
tr:nth-child(even) {
 background-color: #f2f2f2;
}
Step 2: Enabling Scrolling
Since the <table>
element itself does not support scrolling, we need to wrap the table in a div
container that handles overflow.
<div className="table-container">
 <table>{/* Table content */}</table>
</div>
.table-container {
 max-height: 200px;
 max-width: 500px;
 overflow: auto;
 border: 1px solid #ddd;
}
th,
td {
 ...
 white-space: nowrap;
}
In this code:
- We wrap
TableComponent
inside a<div>
with a style that sets a maximum height and width and enables scrolling (overflow: auto
), allowing the table to scroll vertically and horizontally when the content exceeds the specified dimensions. - We set
white-space: nowrap
on th andtd
elements to prevent text wrapping on smaller screens. - We are adding a border around the scrolling div instead of the table to make the scrollbar appear as part of the table.
Here is the basic scrollable table in action:
Implementing a Sticky Header
With the basic scrollable table set up, we can work on the sticky headers by keeping them visible at the top of a table even when the user scrolls down through a long list.
Using CSS position: sticky
The CSS position: sticky
is a powerful feature that toggles between relative and fixed positioning based on the scroll position. It sticks to the top of the scrollable area when content scrolls under it, and it is supported in most modern browsers.
To make the table header sticky, we need to add specific styles to the <thead>
element’s cells.
thead th {
 background-color: #f2f2f2;
 position: sticky;
 top: 0;
 z-index: 1;
}
In this code:
- We set the position of the
<th>
elements inside the<thead>
to sticky. - The
top: 0
property ensures that the header sticks to the top of the scrollable area. - The
z-index: 1
property ensures the header remains above the table content.
Implementing a Sticky Column
As sticky headers provide a constant visual reference for the data rows, sticky columns are equally beneficial when navigating through wide tables requiring horizontal scrolling.
Implementing sticky columns involves a similar approach to sticky headers but targets horizontal positioning.
Let’s modify the existing styles to include sticky columns. We’ll start with making the first column sticky:
th:first-child,
td:first-child {
 position: sticky;
 left: 0;
 z-index: 2;
 background-color: #f2f2f2;
}
th:first-child {
 z-index: 3;
}
In this code:
- We target the first
<th>
and<td>
elements in the table. - We set the position of the first column to
sticky
. - The
left: 0
property ensures that the first column sticks to the left side of the scrollable area. - The
z-index
property ensures that the first column remains above the header row.
To make multiple columns sticky, we can adjust the left value of each subsequent column, accounting for the width of the preceding columns. This requires calculating or setting fixed widths for these columns:
th:first-child,
td:first-child {
 position: sticky;
 left: 0;
 z-index: 2;
 background-color: #f2f2f2;
}
th:nth-child(2),
td:nth-child(2) {
 position: sticky;
 left: 32px; /* Adjust based on the width of the first column */
 z-index: 2;
 background-color: #f2f2f2;
}
th:first-child,
th:nth-child(2) {
 z-index: 3;
}
/* Repeat for additional columns */
Below is the complete code with a live demo:
import "./App.css";
const students = [
 {
  id: 1,
  name: "John Doe",
  age: 20,
  course: "Business",
  grade: "A",
  attendance: "Present",
  status: "Active",
  remarks: "Good",
 },
 {
  id: 2,
  name: "Jane Smith",
  age: 22,
  course: "Engineering",
  grade: "B",
  attendance: "Absent",
  status: "Inactive",
  remarks: "Average",
 },
 {
  id: 3,
  name: "Alice Johnson",
  age: 21,
  course: "Medicine",
  grade: "A+",
  attendance: "Present",
  status: "Active",
  remarks: "Excellent",
 },
 {
  id: 4,
  name: "Bob Brown",
  age: 23,
  course: "Computer Science",
  grade: "C",
  attendance: "Present",
  status: "Active",
  remarks: "Fair",
 },
 {
  id: 5,
  name: "Eve Wilson",
  age: 24,
  course: "Arts",
  grade: "B+",
  attendance: "Absent",
  status: "Inactive",
  remarks: "Good",
 },
 {
  id: 6,
  name: "David Lee",
  age: 25,
  course: "Law",
  grade: "A-",
  attendance: "Present",
  status: "Active",
  remarks: "Very Good",
 },
 {
  id: 7,
  name: "Grace Clark",
  age: 26,
  course: "Science",
  grade: "D",
  attendance: "Absent",
  status: "Inactive",
  remarks: "Poor",
 },
];
const TableComponent = () => {
 return (
  <div className="table-container">
   <table>
    <thead>
     <tr>
      <th>ID</th>
      <th>Name</th>
      <th>Age</th>
      <th>Course</th>
      <th>Grade</th>
      <th>Attendance</th>
      <th>Status</th>
      <th>Remarks</th>
     </tr>
    </thead>
    <tbody>
     {students.map(student => (
      <tr key={student.id}>
       <td>{student.id}</td>
       <td>{student.name}</td>
       <td>{student.age}</td>
       <td>{student.course}</td>
       <td>{student.grade}</td>
       <td>{student.attendance}</td>
       <td>{student.status}</td>
       <td>{student.remarks}</td>
      </tr>
     ))}
    </tbody>
   </table>
  </div>
 );
};
export default TableComponent;
.table-container {
 max-height: 200px;
 max-width: 500px;
 overflow: auto;
 border: 1px solid #ddd;
}
table {
 width: 100%;
 border-collapse: collapse;
 font-family: Arial, sans-serif;
}
th,
td {
 padding: 8px;
 text-align: left;
 border-bottom: 1px solid #ddd;
 white-space: nowrap;
}
thead th {
 background-color: #f2f2f2;
 position: sticky;
 top: 0;
 z-index: 1;
}
tr:nth-child(even) {
 background-color: #f2f2f2;
}
th:first-child,
td:first-child {
 position: sticky;
 left: 0;
 z-index: 2;
 background-color: #f2f2f2;
}
th:nth-child(2),
td:nth-child(2) {
 position: sticky;
 left: 32px; /* Adjust based on the width of the first column */
 z-index: 2;
 background-color: #f2f2f2;
}
th:first-child,
th:nth-child(2) {
 z-index: 3;
}
/* Repeat for additional columns */
Conclusion
In this tutorial, we learned how to create a scrollable table with sticky headers and columns in React. By combining CSS position: sticky
with specific z-index
values, we achieved a fixed header and column effect that enhances the user experience when navigating through large datasets.
Bye for now, and happy coding!