Footers seem trivial until they misbehave. On a page with sparse content the footer hovers awkwardly in the middle of the screen. On a page with a contact form the footer flashes over the input fields. On mobile it hides behind the browser chrome. Each of these is a different problem with a different solution, and conflating them is where most developers go wrong.
This post covers three distinct footer placement patterns — the sticky-to-bottom layout footer, the fixed viewport footer, and the anchored site footer — using Tailwind CSS v4 utilities and, where Tailwind falls short, plain CSS inside a @theme or @layer block. By the end you will know exactly which pattern to reach for and why.
First, clarify what "footer placement" actually means
There are three different things people mean when they say they want the footer "at the bottom":
- Sticky-to-viewport-bottom on short pages, scrolling normally on long pages. This is the classic "sticky footer" problem — the footer should never float in empty whitespace, but it also should not cover content.
- Fixed to the viewport at all times — cookie banners, chat widgets, persistent nav bars. These always sit at a fixed position regardless of scroll.
- Anchored at the bottom of a section or card — a card footer with a CTA that always aligns at the same vertical position across a row of unequal-height cards.
Each calls for a different CSS strategy. Let us go through them one at a time.
Pattern 1: The sticky-to-bottom page footer (flexbox)
This is the most common requirement. On a landing page with a short hero and no sidebar content, the footer should sit at the viewport bottom. On a long blog post it scrolls naturally into view.
The solution is a full-height flex column on the <body> or outermost wrapper, with the main content area set to grow and fill available space:
<body class="flex flex-col min-h-screen">
<header>...</header>
<main class="flex-1">...</main>
<footer>...</footer>
</body>
How it works: min-h-screen ensures the body is at least as tall as the viewport. flex flex-col stacks children vertically. flex-1 on <main> is shorthand for flex: 1 1 0, which tells the element to grow and absorb all remaining space. The header and footer keep their natural heights, and the main region expands to fill whatever is left. When content exceeds the viewport, the body grows taller and the footer simply scrolls into view.
In Tailwind CSS v4 nothing about this changes syntactically — flex-1, flex-col, and min-h-screen all map to the same CSS they always have. What does change in v4 is how you define custom spacing or breakpoints if you need to tweak the layout: you use the CSS-first @theme directive in your stylesheet rather than tailwind.config.js:
@import "tailwindcss";
@theme {
--spacing-footer: 5rem;
}
With the above, Tailwind v4 automatically generates pt-footer, mb-footer, and every other spacing utility for that token — no plugin, no config file.
Pattern 1 (alternative): CSS Grid with named rows
Grid gives you a slightly more explicit layout model. Instead of relying on a child growing, you declare the row sizes up front:
<body class="grid min-h-screen" style="grid-template-rows: auto 1fr auto">
<header>...</header>
<main>...</main>
<footer>...</footer>
</body>
Here auto 1fr auto means: header takes its natural height, main takes all remaining space (1fr), footer takes its natural height. The inline style is needed because Tailwind v4's grid-rows-* utilities cover named patterns like grid-rows-3 (equal thirds) but not arbitrary auto 1fr auto out of the box. The cleanest approach is to push the value into a theme token:
@import "tailwindcss";
@theme {
--grid-template-rows-page: auto 1fr auto;
}
Tailwind v4 will generate a grid-rows-page utility from this, so your HTML stays class-based:
<body class="grid grid-rows-page min-h-screen">
<header>...</header>
<main>...</main>
<footer>...</footer>
</body>
Both the flexbox and grid approaches produce visually identical results on a simple three-region page. Prefer flexbox when you just need the footer pushed down; prefer grid when the layout has more regions or you want row naming for clarity.
Pattern 2: The fixed viewport footer
Cookie consent banners, chat launcher buttons, and "back to top" links need to stay glued to the viewport regardless of scroll position. For this you want position: fixed, not sticky-to-page-bottom.
<div class="fixed bottom-0 left-0 right-0 z-50 bg-white border-t border-gray-200 p-4">
<p class="text-sm text-center">
We use cookies.
<button class="underline font-medium">Accept</button>
</p>
</div>
Key points:
fixed bottom-0 left-0 right-0pins the element to the viewport bottom edge, spanning its full width.z-50layers it above most content. Adjust if you have modals or drawers with higher z-index values.- Add matching bottom padding to
<body>or<main>equal to the banner height, otherwise the fixed element will obscure the last few lines of page content. A utility likepb-16or a CSS custom property makes this easy to keep in sync.
A common mistake is using position: sticky; bottom: 0 here. sticky keeps an element within its scroll container — it only sticks when the user has scrolled past it. On a short page where the element never scrolls out of view, sticky and static look identical. On a long page, a sticky footer inside <main> will disappear as the user scrolls past it. Use fixed when the element must always be visible regardless of scroll depth.
Pattern 3: Card footer alignment across unequal-height cards
This pattern is distinct from page-level footer placement but it trips up even experienced developers. Picture a row of three feature cards: each has a heading, a short description, and a "Learn more" link at the bottom. The descriptions vary in length, so the cards have different heights, and the "Learn more" links sit at different vertical positions — which looks inconsistent and amateurish.
The fix is to make each card a flex column and push the footer of the card to the bottom with mt-auto:
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<article class="flex flex-col border rounded-xl p-6">
<h3 class="text-lg font-semibold">Fast builds</h3>
<p class="mt-2 text-gray-600 flex-1">
Incremental builds finish in microseconds, not seconds.
</p>
<a href="/features/builds" class="mt-4 text-blue-600 font-medium">
Learn more →
</a>
</article>
<article class="flex flex-col border rounded-xl p-6">
<h3 class="text-lg font-semibold">Zero config</h3>
<p class="mt-2 text-gray-600 flex-1">
Drop in the CSS import and start writing utilities immediately.
No tailwind.config.js, no PostCSS plugin wrangling.
</p>
<a href="/features/config" class="mt-4 text-blue-600 font-medium">
Learn more →
</a>
</article>
<article class="flex flex-col border rounded-xl p-6">
<h3 class="text-lg font-semibold">Modern CSS</h3>
<p class="mt-2 text-gray-600 flex-1">
Built on cascade layers, oklch colors, and container queries.
</p>
<a href="/features/css" class="mt-4 text-blue-600 font-medium">
Learn more →
</a>
</article>
</div>
The grid makes all three cards the same height (grid cells in a row share the tallest cell's height). Inside each card, flex flex-col stacks the children vertically and flex-1 on the paragraph lets it absorb extra space, pushing the link to a consistent bottom position. This is the same mental model as the page-level sticky footer — just scoped to a component.
When to reach beyond Tailwind utilities
Tailwind's utility classes cover the vast majority of layout needs, but there are cases where raw CSS is cleaner:
- Safe area insets on mobile: Devices with a home indicator bar (iPhone X and later) need
padding-bottom: env(safe-area-inset-bottom)on fixed footers to avoid being obscured. Tailwind does not have a built-in utility for this, but you can add it with a@layerrule or a@themetoken that wraps the env() value. - Scroll-driven animations: If a footer should fade in when it enters the viewport, CSS
@keyframeswithanimation-timeline: scroll()is more appropriate than toggling Tailwind classes from JavaScript. - Complex multi-footer layouts: Some dashboards have a persistent global footer and a contextual panel footer. Model these as two separate positioned elements rather than fighting a single layout strategy into double duty.
In Tailwind v4 you can co-locate these one-off CSS rules directly in the same stylesheet where you import Tailwind, using @layer utilities or @layer components. Nothing needs to move to a separate file:
@import "tailwindcss";
@layer utilities {
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
}
Common mistakes and how to avoid them
Using position: absolute; bottom: 0 on the footer. This only works if the parent has position: relative and a defined height. On a page without explicit height constraints, an absolutely positioned footer will overlap content. Use the flexbox or grid approach instead.
Setting a fixed height on the footer. Footer content — navigation links, legal disclaimers, newsletter signup fields — changes over time and wraps differently on small screens. Use padding to control interior spacing; let the footer set its own height naturally.
Confusing position: sticky with the "sticky footer" pattern. position: sticky; bottom: 0 on a <footer> makes the footer stick to the bottom of its scroll container as you scroll past it — like a sticky table header in reverse. This is not the same as keeping the footer at the viewport bottom on short pages. For that you need the min-height + flex/grid approach on the wrapper.
Forgetting to account for the fixed footer's height in the document flow. A fixed element is taken out of the normal flow, so the page behind it renders as if the footer does not exist. Always add bottom padding to the scrollable content equal to the footer height.
Putting it together in an Astro layout
In an Astro project, a base layout component is the natural place to set the sticky-footer wrapper once, so every page inherits it automatically:
---
// src/layouts/BaseLayout.astro
const { title } = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>{title}</title>
</head>
<body class="flex flex-col min-h-screen bg-white text-gray-900">
<header><slot name="header" /></header>
<main class="flex-1"><slot /></main>
<footer><slot name="footer" /></footer>
</body>
</html>
Every page then inherits correct footer behaviour without repeating the layout classes. If a page needs a fixed cookie banner on top of this, add it as a separate component inside the default slot — it will sit in the normal flow visually but be positioned relative to the viewport via fixed.
Quick decision guide
- Footer should sit at viewport bottom on short pages, scroll normally on long pages: flex-col + min-h-screen + flex-1 on main.
- Footer must always be visible regardless of scroll: position fixed + bottom-0 + z-index + compensating padding on content.
- CTAs at the bottom of equal-height cards: flex-col on each card + flex-1 on the body text.
- Complex row definitions or named regions: CSS Grid with grid-template-rows via a @theme token.
Footer placement is one of those problems that looks like a detail and turns out to be load-bearing. Get the layout model right once, encode it in a layout component or a shared class, and every page on the site benefits automatically. The flexbox approach has been the reliable default for years and remains so in Tailwind v4 — but understanding when to reach for grid, when to use fixed, and when to write a few lines of plain CSS is what separates layouts that just work from layouts that hold up under real content.