For many developers, implementing a sticky header on a table before the CSS property position: sticky was challenging. In this article, we are going to look at building a sticky header using React Hooks.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
When creating a sticky header for tables, most frontend developers use the <table /> component and the CSS position property, however, in this post we are going to build a table with a sticky header using native React Hooks.
This tutorial assumes that the reader has:
Begin by installing the official React application scaffold tool, create-react-app:
npm install -g create-react-app
You can verify the installation by running create-react-app this should ask you to specify the directory name, in our case, we will be building a table of countries with a sticky header that’s built by React Hooks. We will be building the application as a <Table /> component that takes in the data for the header table and the data for the application, in our case, the <Table /> will take in a list of countries and some information about the country when hovered on by the user.
React allows the use of hooks with functional components, and as a result, we will be creating the following files:
Table.js — This component will contain the table and sticky hook for our applicationData.js — This component will contain the data to be passed as props in our applicationstickyHeader.js — This will be a custom hook for making our header stickyWe will be using create-react-app to bootstrap our project. To create a new project using the create-react-app boilerplate, run the command in your preferred terminal:
create-react-app sticky-header-app
The name “sticky-header-app” is used as our project name for this tutorial, it can be replaced with whatever name you choose to use.
Next, navigate into your project directory and start your development server by running:
cd sticky-heade-app && npm start
The command above opens up a browser tab that will display the default boilerplate application.
In the src folder, let’s create a new file Data.js. Note that the file can be whatever you want and how you see fit.
Here, we export a constant HeaderTable, an array with country, code, and area as objects, next we export another array of objects that contain a few countries, code, and their total land area:
export const HeaderTable = ["Country", "Code", "Area"];
export const tableData = [
{
country: "Albania",
code: "ALB",
area: "28748 km2"
},
{
country: "Algeria",
code: "AG",
area: "2381740 km2"
},
{
country: "Brazil",
code: "BR",
area: "8,515,767 km2",
},
{
country: "Bulgaria",
code: "BG",
area: "110,993.6 km2",
},
{
country: "China",
code: "CN",
area: "9,596,961 km2",
},
{
country: "Denmark",
code: "DK",
area: "42,933 km2",
},
{
country: "USA",
code: "US",
area: "9,833,520 km2",
}
];
export const countryData = [
{
country: "China",
data: `China, is a country
in East Asia. It is the world's most populous country, with a
population of around 1.4 billion in 2019.`
},
{
country: "Albania",
data: `The land was first settled by a tribe of people called
the Illyrians in about 2000 BC. The Romans were one of the
first empires to conquer the land and Albania was part of first
the Roman Empire and then later the Byzantine Empire or Eastern Roman Empire.`
},
{
country: "Algeria",
data: `In ancient times Algeria was known as Numidia.
The Numidians were known for their army which rode horses, or cavalry.
During the Middle Ages, Algeria was lead by various tribes and Berber dynasties`
},
{
country: "Brazil",
data: `Brazil was officially "discovered" in 1500, when a fleet commanded by
Portuguese diplomat Pedro Álvares Cabral, on its way to India, landed in Porto
Seguro, between Salvador and Rio de Janeiro.`
},
{
country: "Bulgaria",
data: `Bulgarian history really starts with the Thracians.
The Thracians were a group of Indo-European tribes that
lived throughout the Balkan Peninsula from about 1000 BC.`
},
{
country: "Denmark",
data: `Denmark has been inhabited by the Danes since its early prehistoric history. The Vikings rose to power during the 9th century.
Over the next 300 years Vikings would raid and explore areas around Denmark all the way to the island of England.`
},
{
country: "USA",
data: `The United States of America (USA) is a sovereign country in North America.
It is commonly called the United States (U.S.) or America. There are forty-eight states
that border each other and Washington, D.C., the capital district. These states are between
the Pacific and Atlantic Oceans.`
},
]
In the code above, we initialized an array called HeaderTable in which the data for our application which includes country, their code, and area. We also exported a data object which includes some information on the country. Next, we will build our custom StickyHeader hook.
StickyHeader hookIn this component, we will build a custom hook sticky header for our header table, we will add event listeners to handle scroll and enforce stickiness:
import { useState, useEffect, useRef, useCallback } from "react";
const StickyHeader = (defaultSticky = false) => {
const [isSticky, setIsSticky] = useState(defaultSticky);
const tableRef = useRef(null);
const toggleSticky = useCallback(
({ top, bottom }) => {
if (top <= 0 && bottom > 2 * 68) {
!isSticky && setIsSticky(true);
} else {
isSticky && setIsSticky(false);
}
},
[isSticky]
);
useEffect(() => {
const handleScroll = () => {
toggleSticky(tableRef.current.getBoundingClientRect());
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [toggleSticky]);
return { tableRef, isSticky };
};
export default StickyHeader;
In the code block above, we imported the native React useState update the initial state of the application, which we’ve defined as isSticky, next we initialized the object tableRef as a reference to the table element.
We initialized a constant handleScroll to detail the position of the table on a screen and check for the current size of the header. The number 65 is the height of the header, the if…else statement simply checks for the value of isSticky and returns a new value if it changes.
Finally, we added an event listener to handle the user’s scroll events, next we’ll build the Table component to handle the user data as props and render our application.
In this component, we will be building a functional component, that takes in parameters from our StickyHeader component and renders our application:
import React, { useState } from "react";
import StickyHeader from "./StickyHeader.js";
import { countryData } from "./Data";
export default function Table({ headers = [], data = [] }) {
const { tableRef, isSticky } = StickyHeader();
const [display, setDisplay] = useState(false);
const [countryDetails, setData] = useState({
countryHistroy: null,
});
const openDetails= (e) => {
countryData.forEach(details => {
if (details.country === e.target.textContent) {
setData({
countryHistroy: details.data,
})
}
});
setDisplay(true);
};
const closeDetails= () => {
setDisplay(false);
};
const renderHeader = () => (
<thead>
<tr>
{headers.map((item) => (
<th key={item}>{item}</th>
))}
</tr>
</thead>
);
return (
<div>
{isSticky && (
<table
className="sticky"
style={{
position: "fixed",
top: 0,
left: 0,
right: 0
}}
>
{renderHeader()}
</table>
)}
<table ref={tableRef}>
{renderHeader()}
<tbody>
{data.map((item) => (
<tr key={item.code}>
<td className="country" onMouseEnter={openDetails} onMouseOut={closeDetails}>{item.country}</td>
<td>{item.code}</td>
<td>{item.area}</td>
</tr>
))}
</tbody>
</table>
<div className="country-details" style={{display: display ? "block" : "none"}}>
<p className="country-description">
{countryDetails.countryHistroy}
</p>
</div>
</div>
);
}
In this component, we initialized the StickyHeader component using two parameters, tableRef and isSticky, the tableRef references the table element where we’ll have a sticky header while the isSticky checks the scroll events in the page.
The <table /> component takes the .sticky style to differentiate the sticky header from the original table, to conclude our application we’ll need to update our App.js file to render our individual components using the Table component like this:
import React from "react";
import Table from "./Table";
import { HeaderTable, tableData } from "./Data";
import "./styles.css";
export default function App() {
return (
<div className="App">
<Table headers={HeaderTable} data={tableData} />
</div>
);
}
In the code block above, we are first importing our HeaderTable and tableData , next, we rendered the Table component, passing the headerTable and tableData as props.
At the end of this, our app header should remain sticky while we scroll through our countries table, it should look like the video below:

In this post, we’ve learned how to use hooks to build a sticky header, we’ve also seen how to pass data from components and render it as props. You can read more React articles here on the LogRocket blog. The code used in this article can be found on CodeSandbox.
Install LogRocket via npm or script tag. LogRocket.init() must be called client-side, not
server-side
$ npm i --save logrocket
// Code:
import LogRocket from 'logrocket';
LogRocket.init('app/id');
// Add to your HTML:
<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>

Examine AgentKit, Open AI’s new tool for building agents. Conduct a side-by-side comparison with n8n by building AI agents with each tool.

AI agents powered by MCP are redefining interfaces, shifting from clicks to intelligent, context-aware conversations.

Learn how platform engineering helps frontend teams streamline workflows with Backstage, automating builds, documentation, and project management.

Build an AI assistant with Vercel AI Elements, which provides pre-built React components specifically designed for AI applications.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
One Reply to "Using React Hooks to create sticky headers"
I recommend passing the ref to the hook instead of from the hook to the implementation. It’s a little more resilient bc you know exactly what the ref is.