TLDR
When designing web layouts, one of the most common challenges developers face is creating scrollable areas that work seamlessly with dynamic content. You probably must have faced the issue where you want only a part of the page to be scrollable(especially vertically), and to take remaining vertical space of the viewport height, but after you are done scrolling the content of the section, you get another scroll behaviour either from a parent container or the default browser vertical scrollbars. You've probably seen GitHub's project boards where columns scroll independently without affecting the header or causing layout shifts. Most people assume they're using fixed heights, but there's a much more elegant solution.
The Problem with Fixed Heights
Let's start with what doesn't work well:
/* ā This approach has problems */
.scrollable-column {
height: 400px; /* Fixed height */
overflow-y: auto;
}
Why fixed heights fail:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Parent container(or viewport) ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā Header (Dynamic content or 64px) ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā Flexible Container ā ā
ā ā Dynamic content ā ā
ā ā ā ā
ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā
ā ā ā Scrollable Content ā ā ā
ā ā ā (h-[600px]) ā ā ā
ā ā ā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā (Content exceeds container height) ā ā ā
ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Unintended Browser/Parent Scroll
(Triggers after completing child/sibling content scroll)
- Poor User Experience: Unexpected scrollbars confuse users and disrupt navigation.
- Visual Clutter: Multiple scrollbars create a cluttered, less appealing interface.
- Interaction Conflicts: Unintended scrolling causes accidental content shifts.
The Flex Method: A Better Approach
The solution lies in using flexbox properties strategically. Here's the core principle:
Start with a container of known height, then use flex properties to distribute space dynamically.
Step 1: Establish a Known Height Container
/* Container with calculable height */
.page-container {
height: calc(100dvh - 64px); /* 64px = header height */
}
Step 2: Apply the Flex Pattern
<!-- The Pattern -->
<div class="h-[calc(100dvh-64px)] flex flex-col">
<!-- Fixed/Sticky Content -->
<header class="page-header">
<h1>Page Title</h1>
<nav>Navigation here</nav>
</header>
<!-- Flexible Content Container -->
<div class="flex-1 overflow-y-hidden">
<!-- Actual Scrollable Content -->
<div class="h-full overflow-auto">
<!-- Your scrollable content here -->
</div>
</div>
</div>
Visual Breakdown
Let me show you how this works with a diagram:
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
ā Known Height Container ā
ā (100dvh - 64px) ā
ā flex flex-col ā
ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā Fixed Header ā ā
ā ā (takes only needed space) ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
ā ā Flexible Container ā ā
ā ā flex-1 overflow-y-hidden ā ā
ā ā ā ā
ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā
ā ā ā Scrollable Content ā ā ā
ā ā ā h-full overflow-auto ā ā ā
ā ā ā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā ā āāāāāāāāāāāāāāāāāāāāāāāā ā ā ā
ā ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā ā
ā āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā ā
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
Real-World Example: GitHub-Style Board
Here's how to implement a GitHub-style project board:
function ProjectBoard() {
return (
<div className="h-screen flex flex-col">
{/* App Header - Fixed */}
<header className="h-16 bg-gray-900 text-white flex items-center px-4">
<h1>Project Board</h1>
</header>
{/* Board Container */}
<div className="flex-1 overflow-y-hidden">
{/* Horizontal Scrollable Board */}
<div className="h-full overflow-x-auto">
<div className="h-full flex gap-4 p-4 min-w-max">
{/* Column 1 */}
<div className="h-full w-80 bg-gray-100 rounded-lg flex flex-col">
{/* Column Header - Fixed */}
<div className="p-4 border-b">
<h2>To Do</h2>
</div>
{/* Column Content - Scrollable */}
<div className="flex-1 overflow-y-hidden">
<div className="h-full overflow-y-auto p-4 space-y-3">
{/* Cards */}
<div className="bg-white p-3 rounded shadow">Task 1</div>
<div className="bg-white p-3 rounded shadow">Task 2</div>
{/* ... more tasks */}
</div>
</div>
</div>
{/* More columns... */}
</div>
</div>
</div>
</div>
);
}
The Key Classes Explained
Let me break down each Tailwind class and its purpose:
Container Setup
h-[calc(100dvh-64px)]
- Sets known height (viewport minus header)flex flex-col
- Creates vertical flex container
Content Distribution
flex-1
- Takes remaining space after other flex itemsoverflow-y-hidden
- Prevents container from growing beyond allocated space. You only need to use this class if you want the direct child of the element to be scrollable else you can repeat the pattern.h-full
- Makes child take full height offlex-1 overflow-y-hidden
containeroverflow-auto
- Enables scrolling when content overflows
Nested Scrollable Areas
For complex layouts with nested scrollable areas, repeat the pattern:
function ComplexLayout() {
return (
<div className="h-screen flex flex-col">
{/* Header */}
<header className="h-16 bg-blue-600">Header</header>
{/* Main Content */}
<div className="flex-1 overflow-y-hidden flex">
{/* Sidebar */}
<aside className="w-64 bg-gray-200 flex flex-col">
<div className="p-4">Sidebar Header</div>
<div className="flex-1 overflow-y-hidden">
<div className="h-full overflow-y-auto p-4">
{/* Scrollable sidebar content */}
</div>
</div>
</aside>
{/* Main Area */}
<main className="flex-1 overflow-y-hidden flex flex-col">
<div className="p-4 border-b">Page Header</div>
<div className="flex-1 overflow-y-hidden">
<div className="h-full overflow-y-auto p-4">
{/* Scrollable main content */}
</div>
</div>
</main>
</div>
</div>
);
}
Why This Method Works
- No Layout Shifts: Everything is calculated based on available space
- Dynamic Content Friendly: Headers and navigation can change size
- Performant: Browser handles calculations efficiently
- Predictable: Behavior is consistent across different content sizes
Common Mistakes to Avoid
ā Don't do this:
.container {
height: 100vh; /* Ignores header */
overflow: auto;
}
ā Or this:
.scrollable {
max-height: 400px; /* Fixed height again */
overflow-y: auto;
}
ā Do this instead:
.container {
height: calc(100dvh - var(--header-height));
display: flex;
flex-direction: column;
}
.content {
flex: 1;
overflow: hidden;
}
.scrollable {
height: 100%;
overflow-y: auto;
}
Browser Support
The dvh
unit has excellent support in modern browsers, but you can fallback to vh
if needed.
Conclusion
The flex method for creating scrollable layouts is a game-changer. It eliminates the need for fixed heights while providing predictable, responsive behavior. Next time you're building a complex layout with scrollable areas, remember this pattern:
- Known height container with
flex flex-col
- Fixed content takes natural space
- Flexible container with
flex-1 overflow-y-hidden
- Scrollable content with
h-full overflow-auto
Master this pattern, and you'll never struggle with scrollable layouts again.