How to keep the footer at the bottom of the page?
Keep the footer at the bottom of short pages with CSS grid or flexbox, why the wrapper must be the direct parent, and how it differs from a fixed footer.
Make the page wrapper at least as tall as the screen and let the main content take the free space. With CSS grid, it takes a few lines on the wrapper:
<body>
<div class="page">
<header class="site-header">...</header>
<main>...</main>
<footer class="site-footer">...</footer>
</div>
</body>body {
margin: 0;
}
.page {
display: grid;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
min-height: 100dvh;
}The header and footer rows are auto, as tall as their content. The middle row is 1fr, so it takes all the remaining height. On a short page, <main> stretches and pushes the footer to the bottom of the screen. On a long page, the wrapper simply grows and the footer comes after the content, as usual.
The margin: 0 on body matters: the browser's default 8px margin plus a 100dvh wrapper adds up to more than the screen, which gives you a scrollbar on every page. The first min-height is a fallback for older browsers; how to make an element 100% of the screen height explains dvh.
The flexbox version
The same thing with flexbox, using a column and a growing <main>:
.page {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
.page > main {
flex: 1;
}Instead of growing <main>, you can push the footer down with an auto margin. It takes all the free space above the footer:
.site-footer {
margin-top: auto;
}Both work. I use the grid version when the structure is exactly header, main and footer. The flex version doesn't care how many elements there are, which helps when something else ends up in the wrapper, like a cookie banner. In the grid version, an extra child before <main> takes a row, and the 1fr lands on the wrong element.
The wrapper must be the direct parent
This is the gotcha behind most "it doesn't work" cases. Grid rows and flex growing only apply to direct children, so the header, main and footer must sit directly inside the element with display: grid or display: flex. An extra <div> around <main> and <footer> breaks it.
In React apps this wrapper is easy to miss. In a Vite React project, your components render inside <div id="root">. In the Next.js Pages Router, they render inside <div id="__next">. Either put the layout on that element, or render your own wrapper around the three parts. In the Next.js App Router, the root layout controls <body>, so you can put the class there:
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className="page">
<Header />
<main>{children}</main>
<Footer />
</body>
</html>
);
}Check in the dev tools that <Header /> and <Footer /> don't add their own wrapper elements around the <header> and <footer>.
Keep the header visible while scrolling
If the header should stay at the top while the page scrolls, add position: sticky to it. It works inside both layouts:
.site-header {
position: sticky;
top: 0;
z-index: 10;
background: #fff;
}Give it a background, or the content will show through as it scrolls underneath. If sticky doesn't stick, look for an ancestor with overflow: hidden or overflow: auto, which is the usual cause.
Why not position: fixed on the footer
A "sticky footer" in this sense means the footer sits at the bottom of short pages. position: fixed; bottom: 0 is something else: the footer is always on screen, taken out of the page flow, and it covers the end of your content on long pages. You then need a padding-bottom on the page that matches the footer's height, which breaks as soon as the footer's height changes. Use fixed only when you really want a bar that is always visible, like a cookie notice or a mobile action bar.