Sticky Footer Patterns
The "sticky footer" problem: a footer should sit at the bottom of the viewport on short pages, but flow naturally below the content on long pages, never overlapping it. It sounds simple and used to require a genuinely ugly negative-margin hack. Modern layout makes it close to trivial.
The modern answer — flexbox column
html, body {
height: 100%;
margin: 0;
}
body {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
main {
flex: 1 0 auto; /* grows to absorb leftover space, pushing footer down */
}
footer {
flex-shrink: 0;
}min-height: 100dvhonbodyguarantees the flex column is always at least a full viewport tall, even when content is short.flex: 1 0 autoonmainmeans it grows to fill any leftover space — that growth is exactly what pushes the footer to the bottom on short pages.On long pages,
mainsimply grows past the viewport height and the footer follows normally below it — no special-casing needed for either case.
The grid version
Grid expresses the same intent slightly more declaratively: three rows, header and footer sized to content, the middle row absorbing everything else.
body {
display: grid;
grid-template-rows: auto 1fr auto; /* header, main, footer */
min-height: 100dvh;
margin: 0;
}auto 1fr auto row template used in the Holy Grail Layout — the sticky footer is really just the vertical slice of that same pattern.100dvh vs 100vh on mobile
On mobile browsers, the address bar can show or hide as the user scrolls, changing the actually visible viewport height without the layout viewport changing. 100vh historically measured the layout viewport — the larger, address-bar-collapsed size — which could push a sticky footer just off-screen below the visible area on load.
Unit | Measures |
|---|---|
| The largest possible viewport (address bar collapsed) — can overflow the visible area when the bar is showing |
| Smallest viewport (address bar fully expanded) — safest floor value |
| Largest viewport — same idea as legacy |
| Dynamic — tracks the actual visible viewport live as the browser chrome shows/hides |
dvh for anything that must always be fully visible and reachable, like a sticky footer or a full-height app shell — it removes the entire class of "the button is hidden behind the address bar" bugs on mobile Safari and Chrome.Legacy techniques and why they died
Before flexbox and grid were reliable, sticky footers relied on a negative-margin trick: give the footer a fixed height, give a page wrapper a min-height: 100% and a bottom padding equal to the footer height, then pull the footer up with a negative margin equal to its own height.
/* Legacy technique — shown for historical context, avoid in new code */
html, body { height: 100%; margin: 0; }
.wrapper {
min-height: 100%;
margin-bottom: -60px; /* negative of the footer's height */
}
.push {
height: 60px; /* a spacer matching the footer's height */
}
footer {
height: 60px;
}It required the footer's height to be known and fixed in advance — a footer that wraps to two lines on mobile silently broke the whole layout.
It needed an extra "push" spacer element purely to reserve space, adding markup that had no semantic purpose.
Any content overlapping the negative margin by even a pixel (a badge, a shadow) could visually clip against the pulled-up footer.
Comparing the approaches
Technique | Needs known footer height | Extra markup | Recommended |
|---|---|---|---|
Flexbox column | No | No | Yes — most common choice |
Grid | No | No | Yes — equally valid, more declarative |
Negative margin | Yes | Yes (spacer element) | No — legacy only |
Related pages: Holy Grail Layout, Viewport Units (vw, vh, svh, dvh), and flex-grow & flex-shrink.