Full Page Layouts in Tailwind: Header, Main, Footer, Two-Column
Build header, main, footer, and two-column page layouts with Tailwind CSS using flexbox and grid — no custom CSS required.
The Foundation: Full-Viewport Shell
Before you touch a single component, you need a shell that fills the viewport and pushes the footer to the bottom. It's a problem every dev encounters, usually around 2 AM when they notice the footer is floating in the middle of a short-content page. Tailwind makes this dead simple — two utility classes on the root element.
The pattern is min-h-screen flex flex-col on your outermost wrapper. That alone gives you a vertical flex container that stretches at minimum to the full viewport height. Your header and footer stay their natural size; the main content area takes whatever's left.
<div class="min-h-screen flex flex-col">
<header class="h-16 bg-gray-900 text-white"><!-- nav --></header>
<main class="flex-1 px-6 py-8"><!-- content --></main>
<footer class="h-12 bg-gray-800 text-white"><!-- links --></footer>
</div>That flex-1 on main is doing the real work — it's shorthand for flex: 1 1 0%, which tells the element to grow and fill all available space. Worth noting: if you forget flex-1 and use flex-grow instead, you'll get the same result in most cases, but flex-1 also resets flex-shrink and flex-basis, which prevents some weird overflow bugs on smaller viewports.
Honestly, this single pattern covers 80% of the page shells you'll ever build. Get it memorized.
Sticky Header Without JS
A header that stays at the top while the page scrolls is practically a given in 2026. You don't need a scroll listener or any JavaScript for this — sticky top-0 z-50 handles it. The tricky part isn't sticking it; it's making sure the content below doesn't jump under it when the page loads.
Since the sticky header stays in the document flow (unlike fixed), your content won't overlap it — nothing extra needed. That said, if you ever switch to fixed top-0, you'll need to add a spacer or pt-16 to the element below it, otherwise your content starts behind the header. I've seen this burn people who casually swap sticky for fixed and wonder why the first section of their page went missing.
<header class="sticky top-0 z-50 h-16 flex items-center justify-between px-6
bg-white/80 backdrop-blur border-b border-gray-200">
<span class="font-bold text-lg">Logo</span>
<nav class="flex gap-6 text-sm font-medium">
<a href="/" class="hover:text-blue-600 transition-colors">Home</a>
<a href="/blog" class="hover:text-blue-600 transition-colors">Blog</a>
<a href="/pricing" class="hover:text-blue-600 transition-colors">Pricing</a>
</nav>
</header>Notice bg-white/80 backdrop-blur — that frosted glass look is a nice touch so content scrolling underneath doesn't look harsh. If you want to go further with the glass aesthetic, Empire UI's glassmorphism components include pre-built nav variants with blur layers that handle dark mode automatically.
One more thing — z-50 is non-negotiable. Dropdowns, modals, tooltips — everything needs to sort out its stacking context, and your header has to win. Tailwind's default z-scale goes z-0 through z-50, so z-50 is the ceiling without going custom.
Two-Column Layout: Sidebar + Content
The sidebar-plus-content split is the layout pattern most dashboards and docs sites can't live without. Tailwind v3 introduced first-class lg: breakpoints that make collapsing a sidebar on mobile genuinely painless. You're doing flex-col on small screens, flex-row on large — and that's really it.
<div class="flex flex-col lg:flex-row gap-8 max-w-7xl mx-auto px-6 py-8">
<!-- Sidebar -->
<aside class="w-full lg:w-64 shrink-0">
<nav class="flex flex-col gap-2 text-sm">
<a href="#" class="px-3 py-2 rounded-lg hover:bg-gray-100 font-medium">Introduction</a>
<a href="#" class="px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Layouts</a>
<a href="#" class="px-3 py-2 rounded-lg hover:bg-gray-100 font-medium">Components</a>
</nav>
</aside>
<!-- Main content -->
<main class="flex-1 min-w-0">
<article class="prose max-w-none"><!-- content --></article>
</main>
</div>That min-w-0 on the main column is critical and often skipped. Flex items have a minimum width equal to their content by default — so without min-w-0, a wide <pre> block or a long URL can blow out your layout on smaller screens. It's one of those Tailwind gotchas that wastes an hour the first time you hit it.
Quick aside: shrink-0 on the sidebar stops it from squashing when the content area needs space. Without it, both columns will share the available width according to their content ratios, which means your sidebar might shrink to 40px on a 1024px screen. Not great.
In practice, a fixed lg:w-64 works for most sidebars. But if your sidebar content is dynamic — collapsible nav trees, accordion menus — consider using lg:w-72 (288px) to give items room to breathe. Sixteen extra pixels sounds minor until you're trying to read a 60-character nav item on a 240px sidebar.
Grid-Based Two-Column with Proportional Split
Flexbox is great for sidebars where one column has a fixed width, but for proportional splits — like a 60/40 content-plus-preview layout — CSS Grid is cleaner. Tailwind exposes grid columns through grid-cols-* and fractional column sizes via col-span-*.
<!-- 60/40 split that collapses to single column on mobile -->
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
<section class="lg:col-span-3 space-y-6">
<!-- Primary content -->
</section>
<aside class="lg:col-span-2">
<!-- Secondary panel -->
</aside>
</div>The grid-cols-5 trick gives you a 5-unit grid, then you assign 3 units and 2 units — clean 60/40 without any percentage math. If you want 70/30, go grid-cols-10 with col-span-7 and col-span-3. Or use arbitrary values: grid-cols-[2fr_1fr] if you prefer explicit fractions.
Look, the arbitrary value syntax (grid-cols-[2fr_1fr]) is powerful but it breaks out of the design system. If you're on a team, defaulting to the 12-column or 5-column patterns keeps diffs readable and onboarding sane. Save the arbitrary values for one-off hero sections where the design literally won't fit a standard column count.
For more complex grid patterns — bento boxes, masonry-ish cards, asymmetric hero sections — check out the Empire UI blog's bento grid article for real production patterns. The fundamentals here apply, but bento layouts add a layer of row-span tricks that deserve their own treatment.
Responsive Main Content Area
Your main content area needs max-width capping, centered alignment, and horizontal padding that scales with the viewport. Tailwind's container class handles some of this, but it's customizable in tailwind.config.js — and honestly, most projects outgrow the defaults within a week.
<!-- The layout shell with responsive max-width and centered content -->
<main class="flex-1 w-full max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<!-- page content here -->
</main>That padding stack — px-4 sm:px-6 lg:px-8 — is a pattern worth memorizing. On a 320px phone, you're at 16px gutters. At 640px you step to 24px, and at 1024px you land at 32px. These values match what most design systems expect and they avoid that cramped feeling on mid-size tablets. The specific px values matter: going lower than 16px side padding on mobile is a UX problem, not just an aesthetic one.
If you're building a content-heavy page — docs, a blog post, a landing page — you'll also want to constrain the prose width separately from the layout. max-w-prose (roughly 65ch, around 660px) is Tailwind's built-in reading width and it's based on actual typographic research. Wrap your article body in it even if the outer layout is wider.
<main class="flex-1 max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="max-w-prose mx-auto">
<!-- Blog post body, docs content, etc -->
</div>
</main>Footer: Columns, Centering, and Sticking to the Bottom
Footers are where layouts quietly break. You've got the sticky-to-bottom issue we already solved with flex-1 on main. But the footer's internal layout — multi-column links, copyright row, maybe a newsletter input — has its own grid problem. And it needs to collapse gracefully on mobile without looking like a jumbled mess.
<footer class="bg-gray-900 text-gray-400 py-12">
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Top: link columns -->
<div class="grid grid-cols-2 sm:grid-cols-4 gap-8 pb-8 border-b border-gray-700">
<div>
<h3 class="text-white font-semibold mb-3 text-sm">Product</h3>
<ul class="space-y-2 text-sm">
<li><a href="/" class="hover:text-white transition-colors">Components</a></li>
<li><a href="/templates" class="hover:text-white transition-colors">Templates</a></li>
<li><a href="/pricing" class="hover:text-white transition-colors">Pricing</a></li>
</ul>
</div>
<div>
<h3 class="text-white font-semibold mb-3 text-sm">Resources</h3>
<ul class="space-y-2 text-sm">
<li><a href="/blog" class="hover:text-white transition-colors">Blog</a></li>
<li><a href="/tools/gradient-generator" class="hover:text-white transition-colors">Tools</a></li>
</ul>
</div>
</div>
<!-- Bottom: copyright -->
<div class="pt-6 flex flex-col sm:flex-row items-center justify-between gap-4 text-sm">
<span>© 2026 Empire UI. All rights reserved.</span>
<div class="flex gap-4">
<a href="#" class="hover:text-white transition-colors">Privacy</a>
<a href="#" class="hover:text-white transition-colors">Terms</a>
</div>
</div>
</div>
</footer>That grid-cols-2 sm:grid-cols-4 jump is deliberate — 2 columns on mobile keeps things scannable without forcing tiny text. Jumping straight from 1 to 4 columns leaves mobile footers looking sparse and desktop footers cramped. Two columns at 375px, four at 640px: it works.
One more thing — the copyright row uses flex-col sm:flex-row because on mobile, stacking the copyright text above the policy links reads better than trying to cram everything side-by-side in 375px. Small detail, but it's the kind of thing users actually notice on their phone.
Putting It All Together: Full Page Template
Here's the complete shell combining everything above. Drop this into a Next.js layout file or a plain HTML template and you have a solid, production-ready starting point. Customization is just swapping class values — no custom CSS, no additional config.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Page Title</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen flex flex-col bg-white text-gray-900">
<!-- Sticky header -->
<header class="sticky top-0 z-50 h-16 flex items-center justify-between
px-6 bg-white/90 backdrop-blur border-b border-gray-200">
<span class="font-bold text-lg">Brand</span>
<nav class="flex gap-6 text-sm font-medium">
<a href="/" class="hover:text-blue-600">Home</a>
<a href="/pricing" class="hover:text-blue-600">Pricing</a>
<a href="/blog" class="hover:text-blue-600">Blog</a>
</nav>
</header>
<!-- Page body: sidebar + content -->
<main class="flex-1 max-w-screen-xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-10">
<div class="flex flex-col lg:flex-row gap-8">
<!-- Sidebar -->
<aside class="w-full lg:w-64 shrink-0">
<p class="text-xs uppercase tracking-wide text-gray-500 mb-3">Navigation</p>
<nav class="flex flex-col gap-1 text-sm">
<a href="#" class="px-3 py-2 rounded-lg bg-blue-50 text-blue-700 font-medium">Section 1</a>
<a href="#" class="px-3 py-2 rounded-lg hover:bg-gray-100">Section 2</a>
<a href="#" class="px-3 py-2 rounded-lg hover:bg-gray-100">Section 3</a>
</nav>
</aside>
<!-- Content area -->
<div class="flex-1 min-w-0">
<div class="max-w-prose">
<h1 class="text-3xl font-bold mb-4">Page Heading</h1>
<p class="text-gray-600 leading-relaxed">Content goes here.</p>
</div>
</div>
</div>
</main>
<!-- Footer -->
<footer class="bg-gray-900 text-gray-400 py-12">
<div class="max-w-screen-xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-2 sm:grid-cols-4 gap-8 pb-8 border-b border-gray-700">
<div>
<h3 class="text-white font-semibold mb-3 text-sm">Product</h3>
<ul class="space-y-2 text-sm">
<li><a href="/" class="hover:text-white">Components</a></li>
<li><a href="/templates" class="hover:text-white">Templates</a></li>
</ul>
</div>
</div>
<div class="pt-6 flex flex-col sm:flex-row items-center justify-between gap-4 text-sm">
<span>© 2026 Empire UI</span>
<div class="flex gap-4">
<a href="#" class="hover:text-white">Privacy</a>
<a href="#" class="hover:text-white">Terms</a>
</div>
</div>
</div>
</footer>
</body>
</html>This template uses zero custom CSS. Everything is Tailwind utilities. If you're moving to a React or Next.js project, the structure maps directly — the outer div becomes your root layout, header becomes a <Header /> component, and so on. For pre-styled component libraries that slot into this shell naturally, browse components to see what Empire UI offers — things like nav bars, footer grids, and content shells that drop straight in.
Want to extend this with visual effects? You can layer in glassmorphism components for the header backdrop, gradient hero sections using the gradient generator, or an elevated card system with the box shadow generator. The layout shell above is intentionally neutral so those layers go on cleanly.
What makes this pattern solid isn't any single trick — it's that every piece solves exactly one problem. The shell handles viewport height. The header handles stickiness. The main area handles centering and breathing room. The sidebar handles responsive collapse. The footer handles multi-column links and the copyright row. When each responsibility is isolated, you can change any piece without touching the others.
FAQ
Add min-h-screen flex flex-col to your page wrapper and flex-1 to the main element. The main grows to fill leftover space, which naturally pushes the footer down. No JavaScript or absolute positioning needed.
Use flexbox when one column has a fixed width (like a 256px sidebar) and the other fills remaining space. Use grid when you want proportional splits — grid-cols-5 with col-span-3 and col-span-2 gives you a clean 60/40 without percentage math.
Flex items default to min-width: auto, meaning they won't shrink below their content's natural width. Without min-w-0, a long URL or wide code block in your content area can overflow the layout on smaller screens.
sticky is almost always better. It keeps the header in document flow, so your content doesn't shift under it. With fixed, you have to manually add padding-top equal to the header height on the element below it — fragile when the header height changes.