modal-media-viewport-overflow
Modal Media Viewport Overflow
Problem
When building a fixed/fullscreen modal (lightbox, dialog) containing a large media element
(video, image, iframe) with footer content below it (captions, links, action buttons), the
footer content gets pushed below the visible viewport. Since the modal typically locks body
scroll (overflow: hidden), users cannot scroll to reach the hidden content.
The elements are present in the DOM and pass accessibility tree inspections, but they are physically invisible and unreachable.
Context / Trigger Conditions
- A fixed/fullscreen modal or lightbox component
- Large media element (video, image) with
max-h-[Xvh]height constraint - Content below the media (caption text, "Watch on YouTube" link, action buttons)
- Body scroll lock:
document.body.style.overflow = 'hidden' - Outer container uses
justify-center(vertically centers content) - Symptom: Footer content is in the DOM but not visible on screen
- Symptom: Elements show in browser DevTools / accessibility tree but can't be seen
- Symptom: Works fine on very tall viewports, breaks on shorter ones
Solution
The fix uses three CSS techniques together:
1. Viewport-relative max-height on media (leave room for footer)
Instead of a fixed percentage like max-h-[75vh], use a calc expression that
mathematically guarantees space for the footer content:
/* Bad: leaves no room for footer */
.video { max-height: 75vh; }
/* Good: guarantees 10rem for padding + footer */
.video { max-height: calc(100dvh - 10rem); }
Tailwind: max-h-[calc(100dvh-10rem)]
The 10rem budget covers:
- Container padding (~2-4rem top + bottom)
- Gap between media and footer (~0.75rem)
- Caption text (~1.5rem)
- Link/button row (~1.5rem)
Adjust the value based on your actual footer content height.
2. min-h-0 on the media flex child (allow shrinking)
Flexbox items have min-height: auto by default, which prevents them from shrinking
below their intrinsic content height. Override this on the container holding the media:
.video-wrapper { min-height: 0; }
Tailwind: min-h-0
Without this, the flex item refuses to shrink even when the parent has a max-height
constraint, causing overflow.
3. flex-shrink-0 on the footer content (protect from squishing)
Ensure the footer content (caption, links, buttons) never gets compressed to zero:
.footer { flex-shrink: 0; }
Tailwind: flex-shrink-0
This guarantees the video shrinks before the footer content disappears.
4. overflow-y-auto on the modal container (safety net)
Add scrollability to the modal itself as a safety net for very small viewports:
.modal-overlay { overflow-y: auto; }
Tailwind: overflow-y-auto
Complete Pattern (Tailwind CSS)
{/* Outer modal overlay — scrollable safety net */}
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center overflow-y-auto p-4 sm:p-8">
{/* Content wrapper — bounded to viewport */}
<div className="flex flex-col items-center w-full max-w-5xl max-h-[calc(100dvh-3rem)] sm:max-h-[calc(100dvh-5rem)]">
{/* Media container — min-h-0 allows flex shrink */}
<div className="w-full min-h-0 rounded-xl overflow-hidden">
<video className="w-full h-auto max-h-[calc(100dvh-10rem)] object-contain" controls>
<source src={videoSrc} type="video/mp4" />
</video>
</div>
{/* Footer — flex-shrink-0 guarantees visibility */}
<div className="flex-shrink-0 mt-3 flex items-center gap-3">
<p>Caption text</p>
<a href={url}>Watch on YouTube</a>
</div>
</div>
</div>
Plain CSS Equivalent
.modal-overlay {
position: fixed;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow-y: auto;
padding: 1rem;
}
.content-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 64rem;
max-height: calc(100dvh - 3rem);
}
.media-wrapper {
width: 100%;
min-height: 0; /* Critical: allows flex shrink */
}
.media-wrapper video {
width: 100%;
height: auto;
max-height: calc(100dvh - 10rem);
object-fit: contain;
}
.footer {
flex-shrink: 0; /* Critical: never hide this */
margin-top: 0.75rem;
}
Verification
Use Playwright or browser DevTools to verify:
// Check that footer content is within the viewport
const link = await page.locator('[data-testid="youtube-link"]').boundingBox();
const viewport = page.viewportSize();
const linkBottom = link.y + link.height;
console.log(linkBottom <= viewport.height ? 'VISIBLE' : 'CLIPPED');
Test at multiple viewport sizes:
- Desktop: 1280x800
- Tablet: 768x1024
- Mobile: 390x660
- Small mobile: 375x550
Example
From this project's VideoLightbox.tsx — before and after:
Before (broken):
// Video at 75vh pushes caption + YouTube link off-screen
<video className="w-full h-auto max-h-[75vh] object-contain" />
<div className="mt-4">
<p>{caption}</p>
<a href={youtubeUrl}>Watch on YouTube</a> {/* INVISIBLE */}
</div>
After (fixed):
// Content wrapper bounded to viewport
<div className="max-h-[calc(100dvh-3rem)]">
{/* Video shrinks to fit, min-h-0 on wrapper */}
<div className="min-h-0">
<video className="w-full h-auto max-h-[calc(100dvh-10rem)] object-contain" />
</div>
{/* Footer guaranteed visible */}
<div className="flex-shrink-0 mt-3">
<p>{caption}</p>
<a href={youtubeUrl}>Watch on YouTube</a> {/* VISIBLE */}
</div>
</div>
Notes
dvhvsvh: Usedvh(dynamic viewport height) for mobile browsers where the toolbar resizes. Falls back gracefully — all modern browsers support it. On desktop,dvhequalsvh.justify-centergotcha: When the outer flex container usesjustify-centerand content overflows, the top of the content can be clipped above the viewport (not just the bottom). Addingoverflow-y-autoto the container fixes this.- Body scroll lock: Remember that
document.body.style.overflow = 'hidden'prevents users from scrolling the page, butoverflow-y-autoon the modal overlay still allows scrolling within the modal. - Native
<dialog>element: Chrome applies defaultmax-height: calc(100% - 2em - 6px)to<dialog>elements. If using native dialog, you may needmax-height: noneto override this. - This also applies to images: The same pattern works for image lightboxes with captions, download buttons, or EXIF info below the image.
- The
min-h-0fix is the most commonly missed piece — without it, the flex item won't shrink below its content's intrinsic height even withmax-heighton the parent.
References
- Headless UI Dialog Discussion — Fixed height/width and scrollable content
- Flexbox Modal Issue: Can't Scroll to Top of Overflowing Flex Item
- A Responsive Modal With Flexbox and No Media Queries
- Considerations for Styling a Modal — CSS-Tricks
- Styling an HTML dialog modal to take the full height of the viewport
- Making Perfectly Sized, Centered, Scrollable Modals