CSSSticky Footer Patterns

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

CSS
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: 100dvh on body guarantees the flex column is always at least a full viewport tall, even when content is short.

  • flex: 1 0 auto on main means it grows to fill any leftover space — that growth is exactly what pushes the footer to the bottom on short pages.

  • On long pages, main simply 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.

CSS
body {
  display: grid;
  grid-template-rows: auto 1fr auto; /* header, main, footer */
  min-height: 100dvh;
  margin: 0;
}
Note
This is the same 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

vh

The largest possible viewport (address bar collapsed) — can overflow the visible area when the bar is showing

svh

Smallest viewport (address bar fully expanded) — safest floor value

lvh

Largest viewport — same idea as legacy vh but explicit

dvh

Dynamic — tracks the actual visible viewport live as the browser chrome shows/hides

Note
Prefer 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.

CSS
/* 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.

Warning
There is no reason to use the negative-margin technique in a project that supports flexbox or grid (essentially every browser in use today) — it exists here purely so you can recognize and safely replace it if you find it in an older codebase.
Comparing the approaches

Technique

Needs known footer height

Extra markup

Recommended

Flexbox column

No

No

Yes — most common choice

Grid auto 1fr auto

No

No

Yes — equally valid, more declarative

Negative margin

Yes

Yes (spacer element)

No — legacy only